diff --git a/backend/app/api/frames.py b/backend/app/api/frames.py index 8e5e993c..b93a4354 100644 --- a/backend/app/api/frames.py +++ b/backend/app/api/frames.py @@ -1,14 +1,22 @@ +import io import json +import shlex + import requests +import os from http import HTTPStatus -from flask import jsonify, request, Response +from flask import jsonify, request, Response, send_file from flask_login import login_required from . import api from app import redis from app.models.frame import Frame, new_frame, delete_frame, update_frame +from app.models.log import new_log as log from app.models.metrics import Metrics from app.codegen.scene_nim import write_scene_nim +from app.utils.ssh_utils import get_ssh_connection, exec_command, remove_ssh_connection +from scp import SCPClient +from tempfile import NamedTemporaryFile @api.route("/frames", methods=["GET"]) @@ -127,6 +135,90 @@ def api_frame_scene_source(id: int, scene: str): return jsonify({'source': write_scene_nim(frame, scene_json)}) return jsonify({'error': f'Scene {scene} not found'}), HTTPStatus.NOT_FOUND +@api.route('/frames//assets', methods=['GET']) +@login_required +def api_frame_get_assets(id: int): + frame = Frame.query.get_or_404(id) + assets_path = frame.assets_path or "/srv/assets" + ssh = get_ssh_connection(frame) + command = f"find {assets_path} -type f -exec stat --format='%s %Y %n' {{}} +" + output = [] + exec_command(frame, ssh, command, output, log_output=False) + remove_ssh_connection(ssh) + + assets = [] + for line in output: + parts = line.split(' ', 2) + size, mtime, path = parts + assets.append({ + 'path': path.strip(), + 'size': int(size.strip()), + 'mtime': int(mtime.strip()), + }) + + assets.sort(key=lambda x: x['path']) + return jsonify(assets=assets) + +@api.route('/frames//asset', methods=['GET']) +@login_required +def api_frame_get_asset(id: int): + frame = Frame.query.get_or_404(id) + assets_path = frame.assets_path or "/srv/assets" + path = request.args.get('path') + mode = request.args.get('mode', 'download') # Default mode is 'download' + filename = request.args.get('filename', os.path.basename(path)) + + if not path: + return jsonify({'error': 'Path parameter is required'}), HTTPStatus.BAD_REQUEST + + # Normalize and validate the path + normalized_path = os.path.normpath(os.path.join(assets_path, path)) + if not normalized_path.startswith(os.path.normpath(assets_path)): + return jsonify({'error': 'Invalid asset path'}), HTTPStatus.BAD_REQUEST + + try: + ssh = get_ssh_connection(frame) + try: + escaped_path = shlex.quote(normalized_path) + # Check if the asset exists and get its MD5 hash + command = f"md5sum {escaped_path}" + log(frame.id, "stdinfo", f"> {command}") + stdin, stdout, stderr = ssh.exec_command(command) + md5sum_output = stdout.read().decode().strip() + if not md5sum_output: + return jsonify({'error': 'Asset not found'}), HTTPStatus.NOT_FOUND + + md5sum = md5sum_output.split()[0] + cache_key = f'asset:{md5sum}' + + cached_asset = redis.get(cache_key) + if cached_asset: + return send_file( + io.BytesIO(cached_asset), + download_name=filename, + as_attachment=(mode == 'download'), + mimetype='image/png' if mode == 'image' else 'application/octet-stream' + ) + + # Download the file to a temporary file + with NamedTemporaryFile(delete=True) as temp_file: + with SCPClient(ssh.get_transport()) as scp: + scp.get(normalized_path, temp_file.name) + temp_file.seek(0) + asset_content = temp_file.read() + redis.set(cache_key, asset_content, ex=86400 * 30) # Cache for 30 days + return send_file( + io.BytesIO(asset_content), + download_name=filename, + as_attachment=(mode == 'download'), + mimetype='image/png' if mode == 'image' else 'application/octet-stream' + ) + finally: + remove_ssh_connection(ssh) + except Exception as e: + return jsonify({'error': 'Internal Server Error', 'message': str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR + + @api.route('/frames//reset', methods=['POST']) @login_required def api_frame_reset_event(id: int): diff --git a/backend/app/utils/ssh_utils.py b/backend/app/utils/ssh_utils.py index 4b7db225..c502aa85 100644 --- a/backend/app/utils/ssh_utils.py +++ b/backend/app/utils/ssh_utils.py @@ -72,17 +72,19 @@ def get_ssh_connection(frame: Frame) -> SSHClient: return ssh -def exec_command(frame: Frame, ssh: SSHClient, command: str, output: Optional[list[str]] = None, raise_on_error = True) -> int: +def exec_command(frame: Frame, ssh: SSHClient, command: str, output: Optional[list[str]] = None, raise_on_error = True, log_output = True) -> int: log(frame.id, "stdout", f"> {command}") _stdin, stdout, stderr = ssh.exec_command(command) exit_status = None while exit_status is None: while line := stdout.readline(): - log(frame.id, "stdout", line) + if log_output: + log(frame.id, "stdout", line) if output is not None: output.append(line) while line := stderr.readline(): - log(frame.id, "stderr", line) + if log_output: + log(frame.id, "stderr", line) if output is not None: output.append(line) diff --git a/e2e/generated/scene_dataDownloadImage.nim b/e2e/generated/scene_dataDownloadImage.nim index fee9898f..25f774c3 100644 --- a/e2e/generated/scene_dataDownloadImage.nim +++ b/e2e/generated/scene_dataDownloadImage.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_dataDownloadUrl.nim b/e2e/generated/scene_dataDownloadUrl.nim index 787fa6b0..c59d7b36 100644 --- a/e2e/generated/scene_dataDownloadUrl.nim +++ b/e2e/generated/scene_dataDownloadUrl.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_dataLocalImage.nim b/e2e/generated/scene_dataLocalImage.nim index 82ad7b55..1411a03c 100644 --- a/e2e/generated/scene_dataLocalImage.nim +++ b/e2e/generated/scene_dataLocalImage.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_dataNewImage.nim b/e2e/generated/scene_dataNewImage.nim index 7c19c616..1ad1767e 100644 --- a/e2e/generated/scene_dataNewImage.nim +++ b/e2e/generated/scene_dataNewImage.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_dataNewImageNext.nim b/e2e/generated/scene_dataNewImageNext.nim index 21e99cbe..ba3ff39d 100644 --- a/e2e/generated/scene_dataNewImageNext.nim +++ b/e2e/generated/scene_dataNewImageNext.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_dataQR.nim b/e2e/generated/scene_dataQR.nim index 9bd99822..2c31bf2d 100644 --- a/e2e/generated/scene_dataQR.nim +++ b/e2e/generated/scene_dataQR.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_dataResize.nim b/e2e/generated/scene_dataResize.nim index 8d892209..8f0f230e 100644 --- a/e2e/generated/scene_dataResize.nim +++ b/e2e/generated/scene_dataResize.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_logicIfElse.nim b/e2e/generated/scene_logicIfElse.nim index 638a1b61..d2a15641 100644 --- a/e2e/generated/scene_logicIfElse.nim +++ b/e2e/generated/scene_logicIfElse.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_logicSetAsState.nim b/e2e/generated/scene_logicSetAsState.nim index 9448b03a..dc4cf513 100644 --- a/e2e/generated/scene_logicSetAsState.nim +++ b/e2e/generated/scene_logicSetAsState.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderColorFlow.nim b/e2e/generated/scene_renderColorFlow.nim index 69850f7c..e512065e 100644 --- a/e2e/generated/scene_renderColorFlow.nim +++ b/e2e/generated/scene_renderColorFlow.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderColorImage.nim b/e2e/generated/scene_renderColorImage.nim index 87c292d4..066e5c54 100644 --- a/e2e/generated/scene_renderColorImage.nim +++ b/e2e/generated/scene_renderColorImage.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderColorSplit.nim b/e2e/generated/scene_renderColorSplit.nim index e6e25843..b26edf8e 100644 --- a/e2e/generated/scene_renderColorSplit.nim +++ b/e2e/generated/scene_renderColorSplit.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderGradientSplit.nim b/e2e/generated/scene_renderGradientSplit.nim index 7c93f16a..95e3c3b3 100644 --- a/e2e/generated/scene_renderGradientSplit.nim +++ b/e2e/generated/scene_renderGradientSplit.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderImage.nim b/e2e/generated/scene_renderImage.nim index b04ed954..cced452f 100644 --- a/e2e/generated/scene_renderImage.nim +++ b/e2e/generated/scene_renderImage.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderImageBlend.nim b/e2e/generated/scene_renderImageBlend.nim index 328824dd..c47bff1e 100644 --- a/e2e/generated/scene_renderImageBlend.nim +++ b/e2e/generated/scene_renderImageBlend.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderImageMask.nim b/e2e/generated/scene_renderImageMask.nim index eda38cfb..4dba50fc 100644 --- a/e2e/generated/scene_renderImageMask.nim +++ b/e2e/generated/scene_renderImageMask.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderOpacity.nim b/e2e/generated/scene_renderOpacity.nim index 09f00c58..b577f2c3 100644 --- a/e2e/generated/scene_renderOpacity.nim +++ b/e2e/generated/scene_renderOpacity.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderSplitData.nim b/e2e/generated/scene_renderSplitData.nim index 02bc83d5..03d9ad11 100644 --- a/e2e/generated/scene_renderSplitData.nim +++ b/e2e/generated/scene_renderSplitData.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderSplitFlow.nim b/e2e/generated/scene_renderSplitFlow.nim index f153e4e5..0f1ea41c 100644 --- a/e2e/generated/scene_renderSplitFlow.nim +++ b/e2e/generated/scene_renderSplitFlow.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderSplitLoop.nim b/e2e/generated/scene_renderSplitLoop.nim index 6835cb66..47fe1d33 100644 --- a/e2e/generated/scene_renderSplitLoop.nim +++ b/e2e/generated/scene_renderSplitLoop.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderTextOverflow.nim b/e2e/generated/scene_renderTextOverflow.nim index a88477d2..b1d20b98 100644 --- a/e2e/generated/scene_renderTextOverflow.nim +++ b/e2e/generated/scene_renderTextOverflow.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderTextPosition.nim b/e2e/generated/scene_renderTextPosition.nim index 5ae62123..544bda91 100644 --- a/e2e/generated/scene_renderTextPosition.nim +++ b/e2e/generated/scene_renderTextPosition.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderTextRich.nim b/e2e/generated/scene_renderTextRich.nim index 7a620658..0a4889b2 100644 --- a/e2e/generated/scene_renderTextRich.nim +++ b/e2e/generated/scene_renderTextRich.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderTextRichOver.nim b/e2e/generated/scene_renderTextRichOver.nim index c9910872..396960db 100644 --- a/e2e/generated/scene_renderTextRichOver.nim +++ b/e2e/generated/scene_renderTextRichOver.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/e2e/generated/scene_renderTextSplit.nim b/e2e/generated/scene_renderTextSplit.nim index 1c26d30a..3f3fcd52 100644 --- a/e2e/generated/scene_renderTextSplit.nim +++ b/e2e/generated/scene_renderTextSplit.nim @@ -1,7 +1,7 @@ # This file is autogenerated {.warning[UnusedImport]: off.} -import pixie, json, times, strformat, strutils, sequtils, options +import pixie, json, times, strformat, strutils, sequtils, options, algorithm import frameos/types import frameos/channels diff --git a/frontend/src/scenes/frame/panels/Assets/Asset.tsx b/frontend/src/scenes/frame/panels/Assets/Asset.tsx new file mode 100644 index 00000000..89c65f41 --- /dev/null +++ b/frontend/src/scenes/frame/panels/Assets/Asset.tsx @@ -0,0 +1,32 @@ +import { useValues } from 'kea' +import { frameLogic } from '../../frameLogic' +import { useState } from 'react' + +interface AssetProps { + path: string +} + +export function Asset({ path }: AssetProps) { + const { frame } = useValues(frameLogic) + const isImage = path.endsWith('.png') || path.endsWith('.jpg') || path.endsWith('.jpeg') || path.endsWith('.gif') + const [isLoading, setIsLoading] = useState(true) + + return ( +
+ {isImage ? ( + <> + setIsLoading(false)} + onError={() => setIsLoading(false)} + className="max-w-full" + src={`/api/frames/${frame.id}/asset?path=${encodeURIComponent(path)}`} + alt={path} + /> + {isLoading ?
Loading...
: null} + + ) : ( + <>{path} + )} +
+ ) +} diff --git a/frontend/src/scenes/frame/panels/Assets/Assets.tsx b/frontend/src/scenes/frame/panels/Assets/Assets.tsx new file mode 100644 index 00000000..3884f505 --- /dev/null +++ b/frontend/src/scenes/frame/panels/Assets/Assets.tsx @@ -0,0 +1,47 @@ +import { useActions, useValues } from 'kea' +import { frameLogic } from '../../frameLogic' +import { assetsLogic } from './assetsLogic' +import { panelsLogic } from '../panelsLogic' +import { Code } from '../../../../components/Code' +import { CloudArrowDownIcon } from '@heroicons/react/24/outline' + +function humaniseSize(size: number) { + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + let unitIndex = 0 + while (size > 1024 && unitIndex < units.length) { + size /= 1024 + unitIndex++ + } + return `${size.toFixed(2)} ${units[unitIndex]}` +} + +export function Assets(): JSX.Element { + const { frame } = useValues(frameLogic) + const { assetsLoading, assets } = useValues(assetsLogic({ frameId: frame.id })) + const { openAsset } = useActions(panelsLogic({ frameId: frame.id })) + return ( +
+ {assetsLoading ? ( +
Loading assets...
+ ) : ( + + + {assets.map((asset) => ( + + + + + + ))} + +
openAsset(asset.path)} className="hover:underline cursor-pointer"> + {asset.path} + {humaniseSize(asset.size)} + + + +
+ )} +
+ ) +} diff --git a/frontend/src/scenes/frame/panels/Assets/assetsLogic.ts b/frontend/src/scenes/frame/panels/Assets/assetsLogic.ts new file mode 100644 index 00000000..a25def26 --- /dev/null +++ b/frontend/src/scenes/frame/panels/Assets/assetsLogic.ts @@ -0,0 +1,41 @@ +import { actions, afterMount, connect, kea, key, path, props, reducers } from 'kea' + +import { AssetType } from '../../../../types' +import { loaders } from 'kea-loaders' +import { socketLogic } from '../../../socketLogic' + +import type { assetsLogicType } from './assetsLogicType' + +export interface assetsLogicProps { + frameId: number +} + +export const assetsLogic = kea([ + path(['src', 'scenes', 'frame', 'assetsLogic']), + props({} as assetsLogicProps), + connect({ logic: [socketLogic] }), + key((props) => props.frameId), + loaders(({ props }) => ({ + assets: [ + [] as AssetType[], + { + loadAssets: async () => { + try { + const response = await fetch(`/api/frames/${props.frameId}/assets`) + if (!response.ok) { + throw new Error('Failed to fetch assets') + } + const data = await response.json() + return data.assets as AssetType[] + } catch (error) { + console.error(error) + return [] + } + }, + }, + ], + })), + afterMount(({ actions, cache }) => { + actions.loadAssets() + }), +]) diff --git a/frontend/src/scenes/frame/panels/allPanels.tsx b/frontend/src/scenes/frame/panels/allPanels.tsx index 32f0a587..916f6b5b 100644 --- a/frontend/src/scenes/frame/panels/allPanels.tsx +++ b/frontend/src/scenes/frame/panels/allPanels.tsx @@ -1,39 +1,43 @@ +import { Apps } from './Apps/Apps' +import { Asset } from './Assets/Asset' +import { Assets } from './Assets/Assets' +import { Control } from './Control/Control' +import { Debug } from './Debug/Debug' import { Diagram } from './Diagram/Diagram' +import { EditApp } from './EditApp/EditApp' +import { Events } from './Events/Events' import { FrameDetails } from './FrameDetails/FrameDetails' import { FrameSettings } from './FrameSettings/FrameSettings' import { Image } from './Image/Image' import { Logs } from './Logs/Logs' -import { Control } from './Control/Control' -import { SceneState } from './SceneState/SceneState' -import { Apps } from './Apps/Apps' -import { Events } from './Events/Events' +import { Metrics } from './Metrics/Metrics' import { Panel } from '../../../types' -import { EditApp } from './EditApp/EditApp' -import { Debug } from './Debug/Debug' -import { Terminal } from './Terminal/Terminal' -import { SceneSource } from './SceneSource/SceneSource' import { SceneJSON } from './SceneJSON/SceneJSON' -import { Metrics } from './Metrics/Metrics' import { Scenes } from './Scenes/Scenes' +import { SceneSource } from './SceneSource/SceneSource' +import { SceneState } from './SceneState/SceneState' import { Templates } from './Templates/Templates' +import { Terminal } from './Terminal/Terminal' export const allPanels: Record JSX.Element> = { - Diagram, + Action: () =>
, // back button when fullscreen + Apps, + Asset, + Assets, + Control, Debug, + Diagram, + EditApp, + Events, FrameDetails, FrameSettings, Image, Logs, - Control, - SceneState, - Apps, - Events, - EditApp, - Terminal, - SceneSource, - SceneJSON, Metrics, + SceneJSON, Scenes, + SceneSource, + SceneState, Templates, - Action: () =>
, + Terminal, } diff --git a/frontend/src/scenes/frame/panels/panelsLogic.ts b/frontend/src/scenes/frame/panels/panelsLogic.ts index d9c03e3d..2fd8b27b 100644 --- a/frontend/src/scenes/frame/panels/panelsLogic.ts +++ b/frontend/src/scenes/frame/panels/panelsLogic.ts @@ -28,6 +28,7 @@ const DEFAULT_LAYOUT: Record = { { panel: Panel.Terminal, active: false, hidden: false }, { panel: Panel.Debug, active: false, hidden: false }, { panel: Panel.SceneSource, active: false, hidden: false }, + { panel: Panel.Assets, active: false, hidden: false }, ], [Area.BottomRight]: [{ panel: Panel.Image, active: true, hidden: false }], } @@ -55,6 +56,7 @@ export const panelsLogic = kea([ editScene: (sceneId: string) => ({ sceneId }), editSceneJSON: (sceneId: string) => ({ sceneId }), editStateScene: (sceneId: string) => ({ sceneId }), + openAsset: (path: string) => ({ path }), persistUntilClosed: (panel: PanelWithMetadata, logic: AnyBuiltLogic) => ({ panel, logic }), }), reducers({ @@ -140,6 +142,24 @@ export const panelsLogic = kea([ a.panel === Panel.Templates ? { ...a, active: true } : a.active ? { ...a, active: false } : a ), }), + openAsset: (state, { path }) => ({ + ...state, + [Area.TopLeft]: state[Area.TopLeft].find((a) => a.panel === Panel.Asset && a.key === path) + ? state[Area.TopLeft].map((a) => + a.key === path ? { ...a, active: true } : a.active ? { ...a, active: false } : a + ) + : [ + ...state[Area.TopLeft].map((a) => ({ ...a, active: false })), + { + panel: Panel.Asset, + key: path, + active: true, + hidden: false, + closable: true, + metadata: { path }, + }, + ], + }), }, ], fullScreenPanel: [ @@ -148,6 +168,7 @@ export const panelsLogic = kea([ toggleFullScreenPanel: (state, { panel }) => (state && panelsEqual(state, panel) ? null : panel), disableFullscreenPanel: () => null, openTemplates: () => null, + openAsset: () => null, }, ], lastSelectedScene: [ diff --git a/frontend/src/types.tsx b/frontend/src/types.tsx index 2c7b8146..8a489fce 100644 --- a/frontend/src/types.tsx +++ b/frontend/src/types.tsx @@ -78,6 +78,12 @@ export interface LogType { frame_id: number } +export interface AssetType { + path: string + size: number + mtime: number +} + export interface MetricsType { id: string timestamp: string @@ -358,23 +364,25 @@ export enum Area { export enum Panel { Action = 'Action', + Apps = 'Apps', + Asset = 'Asset', + Assets = 'Assets', + Control = 'Control', Debug = 'Debug', Diagram = 'Diagram', + EditApp = 'EditApp', + Events = 'Events', FrameDetails = 'FrameDetails', FrameSettings = 'FrameSettings', Image = 'Image', Logs = 'Logs', - SceneState = 'SceneState', - Control = 'Control', - Apps = 'Apps', - Events = 'Events', Metrics = 'Metrics', - EditApp = 'EditApp', - Terminal = 'Terminal', - SceneSource = 'SceneSource', SceneJSON = 'SceneJSON', Scenes = 'Scenes', + SceneSource = 'SceneSource', + SceneState = 'SceneState', Templates = 'Templates', + Terminal = 'Terminal', } export type PanelWithMetadata = {