From 0f965f2824e3a50dce0b813a774470294ba53182 Mon Sep 17 00:00:00 2001 From: Jordan Phillips Date: Sun, 12 May 2024 12:43:58 +1000 Subject: [PATCH] feat: add encode details page and dialog (#29) * feat: store outputted ffmpeg logs in a encode * feat: raise event on encode progression * feat: use subscription to listen to encode progression changes * feat: add encode dialog to dashboard * feat: add global scroll bar style * feat: raise event on encode outputs and subscribe via encode dialog * feat: add sidebar and split components into separate files * refactor: collocate common ui components into separate directories * feat: add blank encode analytics panel * feat: add encode size comparison widget * fix: use @giantnodes/react v1.0.0-canary.16 components * feat: use intercepting routes to display dialog * feat: add dedicated encode detail page * fix: drop duplicate FileSystemEntryFilterType * fix: rename EncodeFindOne * feat: add encode operation details widget * fix: nextjs unable to build * refactor: use separate encode chip components * feat: add flag to anchor encode output + chip color changes --- app/package.json | 5 +- app/pnpm-lock.yaml | 387 +++++++++++++++++- .../@dialog/(.)encode/[id]/page.tsx | 46 +++ app/src/app/(dashboard)/@dialog/default.tsx | 9 + app/src/app/(dashboard)/encode/[id]/page.tsx | 115 ++++++ app/src/app/(dashboard)/layout.tsx | 13 +- app/src/app/(dashboard)/recipes/page.tsx | 4 +- .../[slug]/explore/[[...path]]/page.tsx | 23 +- .../library/[slug]/explore/layout.tsx | 2 +- .../app/(libraries)/library/[slug]/page.tsx | 2 +- .../encode/chips/EncodeDuration.tsx | 47 +++ .../interfaces/encode/chips/EncodePercent.tsx | 51 +++ .../interfaces/encode/chips/EncodeSpeed.tsx | 62 +++ .../interfaces/encode/chips/EncodeStatus.tsx | 76 ++++ .../interfaces/encode/dialog/EncodeDialog.tsx | 100 +++++ .../encode/dialog/EncodeDialogSidebar.tsx | 28 ++ .../dialog/panels/EncodeAnalyticsPanel.tsx | 34 ++ .../dialog/panels/EncodeScriptPanel.tsx | 66 +++ .../encode/dialog/use-encode-dialog.hook.ts | 30 ++ app/src/components/interfaces/encode/index.ts | 11 + .../encode/widgets/EncodeCommandWidget.tsx | 23 ++ .../encode/widgets/EncodeOperationWidget.tsx | 104 +++++ .../encode/widgets/EncodeOutputWidget.tsx | 53 +++ .../encode/widgets/EncodeSizeWidget.tsx | 94 +++++ .../components/interfaces/explore/index.ts | 1 - .../FileSystemBreadcrumb.tsx} | 12 +- .../interfaces/file-system/index.ts | 1 + app/src/components/interfaces/index.ts | 1 - .../layouts/dashboard/navbar/Navbar.tsx | 2 +- .../dashboard/sidebar/SettingsSidebar.tsx | 37 +- .../layouts/dashboard/sidebar/Sidebar.tsx | 14 +- .../sidebar/SidebarLibrarySegment.tsx | 17 +- .../layouts/library/navbar/Navbar.tsx | 2 +- .../library/sidebar/SettingSidebar.tsx | 15 +- .../layouts/library/sidebar/Sidebar.tsx | 41 +- .../tables/encoded/EncodedTable.tsx | 11 +- .../tables/encoding/EncodingTable.tsx | 29 +- app/src/components/ui/ScrollAnchor.tsx | 18 + .../components/ui/code-block/CodeBlock.tsx | 22 + .../ui/encode-badges/EncodeBadges.tsx | 169 -------- app/src/components/ui/index.ts | 2 +- app/src/libraries/dayjs/index.ts | 2 + app/src/styles/global.css | 33 +- app/src/utilities/numbers.ts | 5 + .../Events/RaiseEncodeOutputtedTopic.cs | 25 ++ .../Events/RaiseEncodeProgressedTopic.cs | 25 ++ .../EncodeOperationOutputtedDataActivity.cs | 50 +++ .../Encodes/Sagas/EncodeStateMachine.cs | 4 + .../Sagas/EncodeStateMachineDefinition.cs | 9 +- .../Encodes/Events/EncodeOutputtedEvent.cs | 12 + .../Encodes/Events/EncodeProgressedEvent.cs | 10 + .../src/Domain/Aggregates/Encodes/Encode.cs | 96 ++++- .../Encodes/Queries/EncodeFindOne.cs | 18 + .../EncodeOutputtedSubscription.cs | 23 ++ .../EncodeProgressedSubscription.cs | 23 ++ .../Types/Encodes/Filters/EncodeFilterType.cs | 14 + .../Types/Encodes/Objects/EncodeType.cs | 8 +- .../Encoding/Jobs/EncodeFileConsumer.cs | 15 + .../Events/EncodeOperationOutputtedEvent.cs | 10 + 59 files changed, 1818 insertions(+), 343 deletions(-) create mode 100644 app/src/app/(dashboard)/@dialog/(.)encode/[id]/page.tsx create mode 100644 app/src/app/(dashboard)/@dialog/default.tsx create mode 100644 app/src/app/(dashboard)/encode/[id]/page.tsx create mode 100644 app/src/components/interfaces/encode/chips/EncodeDuration.tsx create mode 100644 app/src/components/interfaces/encode/chips/EncodePercent.tsx create mode 100644 app/src/components/interfaces/encode/chips/EncodeSpeed.tsx create mode 100644 app/src/components/interfaces/encode/chips/EncodeStatus.tsx create mode 100644 app/src/components/interfaces/encode/dialog/EncodeDialog.tsx create mode 100644 app/src/components/interfaces/encode/dialog/EncodeDialogSidebar.tsx create mode 100644 app/src/components/interfaces/encode/dialog/panels/EncodeAnalyticsPanel.tsx create mode 100644 app/src/components/interfaces/encode/dialog/panels/EncodeScriptPanel.tsx create mode 100644 app/src/components/interfaces/encode/dialog/use-encode-dialog.hook.ts create mode 100644 app/src/components/interfaces/encode/index.ts create mode 100644 app/src/components/interfaces/encode/widgets/EncodeCommandWidget.tsx create mode 100644 app/src/components/interfaces/encode/widgets/EncodeOperationWidget.tsx create mode 100644 app/src/components/interfaces/encode/widgets/EncodeOutputWidget.tsx create mode 100644 app/src/components/interfaces/encode/widgets/EncodeSizeWidget.tsx rename app/src/components/interfaces/{explore/breadcrumbs/ExploreBreadcrumbs.tsx => file-system/FileSystemBreadcrumb.tsx} (84%) create mode 100644 app/src/components/interfaces/file-system/index.ts delete mode 100644 app/src/components/interfaces/index.ts create mode 100644 app/src/components/ui/ScrollAnchor.tsx create mode 100644 app/src/components/ui/code-block/CodeBlock.tsx delete mode 100644 app/src/components/ui/encode-badges/EncodeBadges.tsx create mode 100644 app/src/utilities/numbers.ts create mode 100644 src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeOutputtedTopic.cs create mode 100644 src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeProgressedTopic.cs create mode 100644 src/Service.Dashboard/src/Application.Components/Encodes/Sagas/Activities/EncodeOperationOutputtedDataActivity.cs create mode 100644 src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeOutputtedEvent.cs create mode 100644 src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeProgressedEvent.cs create mode 100644 src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Queries/EncodeFindOne.cs create mode 100644 src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeOutputtedSubscription.cs create mode 100644 src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeProgressedSubscription.cs create mode 100644 src/Service.Dashboard/src/HttpApi/Types/Encodes/Filters/EncodeFilterType.cs create mode 100644 src/Service.Encoder/src/Application.Contracts/Encoding/Events/EncodeOperationOutputtedEvent.cs diff --git a/app/package.json b/app/package.json index 3a96fc89..106ea15a 100644 --- a/app/package.json +++ b/app/package.json @@ -29,7 +29,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@giantnodes/react": "1.0.0-canary.14", + "@giantnodes/react": "1.0.0-canary.16", "@hookform/resolvers": "^3.3.4", "@tabler/icons-react": "^3.3.0", "clsx": "^2.1.1", @@ -43,6 +43,8 @@ "react-hook-form": "^7.51.4", "react-intersection-observer": "^9.10.2", "react-relay": "^16.2.0", + "react-syntax-highlighter": "^15.5.0", + "recharts": "^2.12.7", "relay-runtime": "^16.2.0", "tailwindcss": "^3.4.3", "zod": "^3.23.6" @@ -52,6 +54,7 @@ "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@types/react-relay": "^16.0.6", + "@types/react-syntax-highlighter": "^15.5.13", "@types/relay-runtime": "^14.1.23", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index a55c95a1..c48c9dc2 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@giantnodes/react': - specifier: 1.0.0-canary.14 - version: 1.0.0-canary.14(react-dom@18.3.1)(react@18.3.1)(tailwindcss@3.4.3) + specifier: 1.0.0-canary.16 + version: 1.0.0-canary.16(react-dom@18.3.1)(react@18.3.1)(tailwindcss@3.4.3) '@hookform/resolvers': specifier: ^3.3.4 version: 3.3.4(react-hook-form@7.51.4) @@ -47,6 +47,12 @@ dependencies: react-relay: specifier: ^16.2.0 version: 16.2.0(react@18.3.1) + react-syntax-highlighter: + specifier: ^15.5.0 + version: 15.5.0(react@18.3.1) + recharts: + specifier: ^2.12.7 + version: 2.12.7(react-dom@18.3.1)(react@18.3.1) relay-runtime: specifier: ^16.2.0 version: 16.2.0 @@ -70,6 +76,9 @@ devDependencies: '@types/react-relay': specifier: ^16.0.6 version: 16.0.6 + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 '@types/relay-runtime': specifier: ^14.1.23 version: 14.1.23 @@ -231,15 +240,15 @@ packages: tslib: 2.6.2 dev: false - /@giantnodes/react@1.0.0-canary.14(react-dom@18.3.1)(react@18.3.1)(tailwindcss@3.4.3): - resolution: {integrity: sha512-tR2MMl1nOvZrS9jEwDm86H/+15SH6wb7m1gZrbRuRQkRmmgbzTkMecc9FfadEMa5ldjdHYBMqfgUomucAXXIGA==} + /@giantnodes/react@1.0.0-canary.16(react-dom@18.3.1)(react@18.3.1)(tailwindcss@3.4.3): + resolution: {integrity: sha512-Aq6cjab2OAan3Tb1mLgDxLFz5UYyG/L/77T2lkIV2bttsrRana/DHF29KfZpoNmGEfM5N1gV8LgAuVazyJtP8g==} engines: {node: '>=16.x'} peerDependencies: react: '>=18' react-dom: '>=18' tailwindcss: '>=3' dependencies: - '@giantnodes/theme': 1.0.0-canary.14(tailwindcss@3.4.3) + '@giantnodes/theme': 1.0.0-canary.16(tailwindcss@3.4.3) '@react-aria/utils': 3.24.0(react@18.3.1) clsx: 2.1.1 react: 18.3.1 @@ -250,8 +259,8 @@ packages: tailwindcss-react-aria-components: 1.1.2(tailwindcss@3.4.3) dev: false - /@giantnodes/theme@1.0.0-canary.14(tailwindcss@3.4.3): - resolution: {integrity: sha512-5cYeyX8VMV8oST0uClTxx/WMNaeYYVboKWdb0KDLkI/VytOF3E9jLYoHKW31swdpk9omQmq2W1eMSQ70WFtF8g==} + /@giantnodes/theme@1.0.0-canary.16(tailwindcss@3.4.3): + resolution: {integrity: sha512-e6k+Qx0c3Q0ASQOeIYccyZ4YIIsNdqfLdINOjjI056k6rMS6e/IbUvM1BHjdquSfiS7He9xCRFvllEbjMmsTHg==} engines: {node: '>=16.x'} peerDependencies: tailwindcss: '>=3' @@ -1835,6 +1844,54 @@ packages: resolution: {integrity: sha512-PLVe9d7b59sKytbx00KgeGhQG3N176Ezv8YMmsnSz4s0ifDzMWlp/h2wEfQZ0ZNe8e377GY2OW6kovUe3Rnd0g==} dev: false + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.1.0: + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-shape@3.1.6: + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + dependencies: + '@types/d3-path': 3.1.0 + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + + /@types/hast@2.3.10: + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + dependencies: + '@types/unist': 2.0.10 + dev: false + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -1866,6 +1923,12 @@ packages: '@types/relay-runtime': 14.1.23 dev: true + /@types/react-syntax-highlighter@15.5.13: + resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} + dependencies: + '@types/react': 18.3.1 + dev: true + /@types/react@18.3.1: resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} dependencies: @@ -1881,6 +1944,10 @@ packages: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true + /@types/unist@2.0.10: + resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} + dev: false + /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2437,6 +2504,18 @@ packages: supports-color: 7.2.0 dev: true + /character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + dev: false + + /character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + dev: false + + /character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + dev: false + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -2494,6 +2573,10 @@ packages: color-string: 1.9.1 dev: false + /comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + dev: false + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2529,7 +2612,77 @@ packages: /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} - dev: true + + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2589,6 +2742,10 @@ packages: ms: 2.1.2 dev: true + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -2683,6 +2840,13 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.24.4 + csstype: 3.1.2 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -3404,6 +3568,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3438,6 +3606,11 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: false + /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -3461,6 +3634,12 @@ packages: dependencies: reusify: 1.0.4 + /fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + dependencies: + format: 0.2.2 + dev: false + /fbjs-css-vars@1.0.2: resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} dev: false @@ -3536,6 +3715,11 @@ packages: signal-exit: 4.1.0 dev: true + /format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + dev: false + /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true @@ -3814,6 +3998,24 @@ packages: function-bind: 1.1.2 dev: true + /hast-util-parse-selector@2.2.5: + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + dev: false + + /hastscript@6.0.0: + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 1.0.8 + hast-util-parse-selector: 2.2.5 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + dev: false + + /highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3874,6 +4076,11 @@ packages: side-channel: 1.0.4 dev: true + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /intl-messageformat@10.5.11: resolution: {integrity: sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==} dependencies: @@ -3889,6 +4096,17 @@ packages: loose-envify: 1.4.0 dev: false + /is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + dev: false + + /is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + dev: false + /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -3960,6 +4178,10 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + dev: false + /is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -4000,6 +4222,10 @@ packages: dependencies: is-extglob: 2.1.1 + /is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + dev: false + /is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -4235,12 +4461,23 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true dependencies: js-tokens: 4.0.0 + /lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + dev: false + /lru-cache@10.1.0: resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} engines: {node: 14 || >=16.14} @@ -4584,6 +4821,17 @@ packages: callsites: 3.1.0 dev: true + /parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4744,6 +4992,16 @@ packages: tslib: 2.6.2 dev: true + /prismjs@1.27.0: + resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} + engines: {node: '>=6'} + dev: false + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: false + /promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} dependencies: @@ -4756,7 +5014,12 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: true + + /property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + dependencies: + xtend: 4.0.2 + dev: false /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} @@ -4880,7 +5143,6 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true /react-relay@16.2.0(react@18.3.1): resolution: {integrity: sha512-f/HtC4whyYmK6/WUeOVakXRoBkV+JEgoSeBHXfIC2U6AuH14NrKXnFicX65LksfzgD1OUfYF6IqGQ4MvO52lTQ==} @@ -4897,6 +5159,19 @@ packages: - encoding dev: false + /react-smooth@4.0.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1) + dev: false + /react-stately@3.31.0(react@18.3.1): resolution: {integrity: sha512-G6y7t6qpP3LU4mLM2RlRTgdW5eiZrR2yB0XZbLo8qVplazxyRzlDJRBdE8OBTpw2SO1q5Auub3NOTH3vH0qCHg==} peerDependencies: @@ -4928,6 +5203,33 @@ packages: react: 18.3.1 dev: false + /react-syntax-highlighter@15.5.0(react@18.3.1): + resolution: {integrity: sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==} + peerDependencies: + react: '>= 0.14.0' + dependencies: + '@babel/runtime': 7.24.4 + highlight.js: 10.7.3 + lowlight: 1.20.0 + prismjs: 1.29.0 + react: 18.3.1 + refractor: 3.6.0 + dev: false + + /react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.24.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -4946,6 +5248,31 @@ packages: dependencies: picomatch: 2.3.1 + /recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts@2.12.7(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 16.13.1 + react-smooth: 4.0.1(react-dom@18.3.1)(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + dev: false + /reflect.getprototypeof@1.0.4: resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} engines: {node: '>= 0.4'} @@ -4958,6 +5285,14 @@ packages: which-builtin-type: 1.1.3 dev: true + /refractor@3.6.0: + resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + dependencies: + hastscript: 6.0.0 + parse-entities: 2.0.0 + prismjs: 1.27.0 + dev: false + /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -5170,6 +5505,10 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + /space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + dev: false + /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5413,6 +5752,10 @@ packages: dependencies: any-promise: 1.3.0 + /tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + dev: false + /titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -5612,6 +5955,25 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -5709,6 +6071,11 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true diff --git a/app/src/app/(dashboard)/@dialog/(.)encode/[id]/page.tsx b/app/src/app/(dashboard)/@dialog/(.)encode/[id]/page.tsx new file mode 100644 index 00000000..5f05383c --- /dev/null +++ b/app/src/app/(dashboard)/@dialog/(.)encode/[id]/page.tsx @@ -0,0 +1,46 @@ +'use client' + +import type { page_EncodedDialog_Query } from '@/__generated__/page_EncodedDialog_Query.graphql' + +import { notFound, useRouter } from 'next/navigation' +import { graphql, useLazyLoadQuery } from 'react-relay' + +import { EncodeDialog } from '@/components/interfaces/encode' + +const QUERY = graphql` + query page_EncodedDialog_Query($where: EncodeFilterInput) { + encode(where: $where) { + ...EncodeDialogFragment + } + } +` + +type EncodePageProps = { + params: { + [x: string]: never + } +} + +const EncodePage: React.FC = ({ params }) => { + const router = useRouter() + + const query = useLazyLoadQuery(QUERY, { + where: { + id: { + eq: decodeURIComponent(params.id), + }, + }, + }) + + if (query.encode == null) { + return notFound() + } + + return ( + + Open + + ) +} + +export default EncodePage diff --git a/app/src/app/(dashboard)/@dialog/default.tsx b/app/src/app/(dashboard)/@dialog/default.tsx new file mode 100644 index 00000000..6561c832 --- /dev/null +++ b/app/src/app/(dashboard)/@dialog/default.tsx @@ -0,0 +1,9 @@ +type DialogDefaultProps = { + params: { + id: string + } +} + +const DialogDefault: React.FC = () => null + +export default DialogDefault diff --git a/app/src/app/(dashboard)/encode/[id]/page.tsx b/app/src/app/(dashboard)/encode/[id]/page.tsx new file mode 100644 index 00000000..46196102 --- /dev/null +++ b/app/src/app/(dashboard)/encode/[id]/page.tsx @@ -0,0 +1,115 @@ +'use client' + +import type { page_EncodedPage_Query } from '@/__generated__/page_EncodedPage_Query.graphql' + +import { Alert, Card, Typography } from '@giantnodes/react' +import { IconAlertCircleFilled } from '@tabler/icons-react' +import { notFound } from 'next/navigation' +import { graphql, useLazyLoadQuery } from 'react-relay' + +import { + EncodeCommandWidget, + EncodeOperationWidget, + EncodeOutputWidget, + EncodeSizeWidget, +} from '@/components/interfaces/encode' +import { FileSystemBreadcrumb } from '@/components/interfaces/file-system' + +const QUERY = graphql` + query page_EncodedPage_Query($where: EncodeFilterInput) { + encode(where: $where) { + failure_reason + file { + ...FileSystemBreadcrumbFragment + } + ...EncodeOperationWidgetFragment + ...EncodeCommandWidgetFragment + ...EncodeOutputWidgetFragment + ...EncodeSizeWidgetFragment + } + } +` + +type EncodePageProps = { + params: { + [x: string]: never + } +} + +const EncodePage: React.FC = ({ params }) => { + const query = useLazyLoadQuery(QUERY, { + where: { + id: { + eq: decodeURIComponent(params.id), + }, + }, + }) + + if (query.encode == null) { + return notFound() + } + + return ( +
+
+
+ {query.encode.failure_reason && ( + + + + The encode operation encountered an error + + {query.encode.failure_reason} + + + + )} + + + + + + + + + + Size + + + + + + + + + + Command + + + + + + + + + + Output + + + + + + +
+ +
+ + + +
+
+
+ ) +} + +export default EncodePage diff --git a/app/src/app/(dashboard)/layout.tsx b/app/src/app/(dashboard)/layout.tsx index a65a3546..b898053e 100644 --- a/app/src/app/(dashboard)/layout.tsx +++ b/app/src/app/(dashboard)/layout.tsx @@ -2,9 +2,16 @@ import React from 'react' import DefaultLayout from '@/components/layouts/dashboard/DashboardLayout' -type DashboardLayoutProps = React.PropsWithChildren +type DashboardSegmentLayoutProps = React.PropsWithChildren & { + dialog: React.ReactNode +} -const DashboardLayout: React.FC = ({ children }) => {children} +const DashboardSegmentLayout: React.FC = ({ children, dialog }) => ( + + {children} + {dialog} + +) export const dynamic = 'force-dynamic' -export default DashboardLayout +export default DashboardSegmentLayout diff --git a/app/src/app/(dashboard)/recipes/page.tsx b/app/src/app/(dashboard)/recipes/page.tsx index a0a2b852..83b98c16 100644 --- a/app/src/app/(dashboard)/recipes/page.tsx +++ b/app/src/app/(dashboard)/recipes/page.tsx @@ -21,9 +21,9 @@ const RecipeListPage: React.FC = () => { }) return ( -
+
-
+
Recipes diff --git a/app/src/app/(libraries)/library/[slug]/explore/[[...path]]/page.tsx b/app/src/app/(libraries)/library/[slug]/explore/[[...path]]/page.tsx index d8489ce6..865314cf 100644 --- a/app/src/app/(libraries)/library/[slug]/explore/[[...path]]/page.tsx +++ b/app/src/app/(libraries)/library/[slug]/explore/[[...path]]/page.tsx @@ -8,20 +8,15 @@ import React, { Suspense } from 'react' import { graphql, useLazyLoadQuery } from 'react-relay' import { useLibraryContext } from '@/app/(libraries)/library/[slug]/use-library.hook' -import { - ExploreBreadcrumbs, - ExploreContext, - ExploreControls, - ExploreTable, - useExplore, -} from '@/components/interfaces/explore' +import { ExploreContext, ExploreControls, ExploreTable, useExplore } from '@/components/interfaces/explore' +import { FileSystemBreadcrumb } from '@/components/interfaces/file-system' import { ResolutionWidget } from '@/components/widgets' const QUERY = graphql` query page_LibrarySlugExploreQuery($where: FileSystemDirectoryFilterInput, $order: [FileSystemEntrySortInput!]) { file_system_directory(where: $where) { id - ...ExploreBreadcrumbsFragment + ...FileSystemBreadcrumbFragment ...ExploreControlsFragment ...ExploreTableFragment @arguments(order: $order) } @@ -82,13 +77,13 @@ const LibraryExplorePage: React.FC = ({ params }) => { const context = useExplore({ directory: query.file_system_directory.id }) return ( -
+
-
+
- + @@ -109,10 +104,10 @@ const LibraryExplorePage: React.FC = ({ params }) => {
-
- +
+ - Resolution + Resolution diff --git a/app/src/app/(libraries)/library/[slug]/explore/layout.tsx b/app/src/app/(libraries)/library/[slug]/explore/layout.tsx index 2586b0af..53c29d89 100644 --- a/app/src/app/(libraries)/library/[slug]/explore/layout.tsx +++ b/app/src/app/(libraries)/library/[slug]/explore/layout.tsx @@ -1,7 +1,7 @@ type LibrarySlugExplorePageLayoutProps = React.PropsWithChildren const LibrarySlugExplorePageLayout: React.FC = ({ children }) => ( -
{children}
+
{children}
) export default LibrarySlugExplorePageLayout diff --git a/app/src/app/(libraries)/library/[slug]/page.tsx b/app/src/app/(libraries)/library/[slug]/page.tsx index f64347c0..8ca6b581 100644 --- a/app/src/app/(libraries)/library/[slug]/page.tsx +++ b/app/src/app/(libraries)/library/[slug]/page.tsx @@ -33,7 +33,7 @@ const LibraryDashboard = () => { }) return ( - + Tasks diff --git a/app/src/components/interfaces/encode/chips/EncodeDuration.tsx b/app/src/components/interfaces/encode/chips/EncodeDuration.tsx new file mode 100644 index 00000000..475a6c05 --- /dev/null +++ b/app/src/components/interfaces/encode/chips/EncodeDuration.tsx @@ -0,0 +1,47 @@ +import type { EncodeDurationFragment$key } from '@/__generated__/EncodeDurationFragment.graphql' + +import { Chip } from '@giantnodes/react' +import dayjs from 'dayjs' +import React from 'react' +import { graphql, useFragment } from 'react-relay' + +const FRAGMENT = graphql` + fragment EncodeDurationFragment on Encode { + status + failed_at + cancelled_at + completed_at + created_at + } +` + +type EncodeDurationChipProps = { + $key: EncodeDurationFragment$key +} + +const EncodeDuration: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + const date = React.useMemo(() => { + switch (data.status) { + case 'COMPLETED': + return data.completed_at + case 'CANCELLED': + return data.cancelled_at + + case 'FAILED': + return data.failed_at + + default: + return undefined + } + }, [data.cancelled_at, data.completed_at, data.failed_at, data.status]) + + return ( + + {dayjs.duration(dayjs(date).diff(data.created_at)).format('H[h] m[m] s[s]')} + + ) +} + +export default EncodeDuration diff --git a/app/src/components/interfaces/encode/chips/EncodePercent.tsx b/app/src/components/interfaces/encode/chips/EncodePercent.tsx new file mode 100644 index 00000000..ded839a2 --- /dev/null +++ b/app/src/components/interfaces/encode/chips/EncodePercent.tsx @@ -0,0 +1,51 @@ +import type { EncodePercentFragment$key } from '@/__generated__/EncodePercentFragment.graphql' + +import { Chip } from '@giantnodes/react' +import React from 'react' +import { graphql, useFragment, useSubscription } from 'react-relay' + +import { percent } from '@/utilities/numbers' + +const FRAGMENT = graphql` + fragment EncodePercentFragment on Encode { + id + percent + } +` + +const SUBSCRIPTION = graphql` + subscription EncodePercentSubscription($where: EncodeFilterInput) { + encode_progressed(where: $where) { + ...EncodePercentFragment + } + } +` + +type EncodePercentProps = { + $key: EncodePercentFragment$key +} + +const EncodePercent: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + useSubscription({ + subscription: SUBSCRIPTION, + variables: { + variables: { + where: { + id: { + eq: data.id, + }, + }, + }, + }, + }) + + if (data.percent == null) { + return undefined + } + + return {percent(data.percent)} +} + +export default EncodePercent diff --git a/app/src/components/interfaces/encode/chips/EncodeSpeed.tsx b/app/src/components/interfaces/encode/chips/EncodeSpeed.tsx new file mode 100644 index 00000000..b3482efb --- /dev/null +++ b/app/src/components/interfaces/encode/chips/EncodeSpeed.tsx @@ -0,0 +1,62 @@ +import type { EncodeSpeedFragment$key } from '@/__generated__/EncodeSpeedFragment.graphql' + +import { Chip } from '@giantnodes/react' +import { filesize } from 'filesize' +import React from 'react' +import { graphql, useFragment, useSubscription } from 'react-relay' + +const FRAGMENT = graphql` + fragment EncodeSpeedFragment on Encode { + id + speed { + frames + bitrate + scale + } + } +` + +const SUBSCRIPTION = graphql` + subscription EncodeSpeedSubscription($where: EncodeFilterInput) { + encode_speed_change(where: $where) { + ...EncodeSpeedFragment + } + } +` + +type EncodeSpeedProps = { + $key: EncodeSpeedFragment$key +} + +const EncodeSpeed: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + useSubscription({ + subscription: SUBSCRIPTION, + variables: { + variables: { + where: { + id: { + eq: data.id, + }, + }, + }, + }, + }) + + if (data.speed == null) { + return undefined + } + + return ( + <> + {data.speed.frames} fps + + {filesize(data.speed.bitrate * 0.125, { bits: true }).toLowerCase()}/s + + {data.speed.scale.toFixed(2)}x + + ) +} + +export default EncodeSpeed diff --git a/app/src/components/interfaces/encode/chips/EncodeStatus.tsx b/app/src/components/interfaces/encode/chips/EncodeStatus.tsx new file mode 100644 index 00000000..c4da260f --- /dev/null +++ b/app/src/components/interfaces/encode/chips/EncodeStatus.tsx @@ -0,0 +1,76 @@ +import type { EncodeStatusFragment$key } from '@/__generated__/EncodeStatusFragment.graphql' +import type { ChipProps } from '@giantnodes/react' + +import { Chip } from '@giantnodes/react' +import dayjs from 'dayjs' +import React from 'react' +import { graphql, useFragment } from 'react-relay' + +const FRAGMENT = graphql` + fragment EncodeStatusFragment on Encode { + status + failed_at + cancelled_at + completed_at + } +` + +type EncodeStatusChipProps = { + $key: EncodeStatusFragment$key +} + +const EncodeStatus: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + const color = React.useMemo(() => { + switch (data.status) { + case 'SUBMITTED': + return 'info' + + case 'QUEUED': + return 'info' + + case 'ENCODING': + return 'success' + + case 'DEGRADED': + return 'warning' + + case 'COMPLETED': + return 'success' + + case 'CANCELLED': + return 'neutral' + + case 'FAILED': + return 'danger' + + default: + return 'neutral' + } + }, [data.status]) + + const title = React.useMemo(() => { + switch (data.status) { + case 'COMPLETED': + return dayjs(data.completed_at).format('L LT') + + case 'CANCELLED': + return dayjs(data.cancelled_at).format('L LT') + + case 'FAILED': + return dayjs(data.failed_at).format('L LT') + + default: + return undefined + } + }, [data.cancelled_at, data.completed_at, data.failed_at, data.status]) + + return ( + + {data.status.toLowerCase()} + + ) +} + +export default EncodeStatus diff --git a/app/src/components/interfaces/encode/dialog/EncodeDialog.tsx b/app/src/components/interfaces/encode/dialog/EncodeDialog.tsx new file mode 100644 index 00000000..f12cb378 --- /dev/null +++ b/app/src/components/interfaces/encode/dialog/EncodeDialog.tsx @@ -0,0 +1,100 @@ +import type { EncodeDialogFragment$key } from '@/__generated__/EncodeDialogFragment.graphql' +import type { DialogProps } from '@giantnodes/react' + +import { Button, Card, Chip, Dialog, Typography } from '@giantnodes/react' +import { IconX } from '@tabler/icons-react' +import React from 'react' +import { graphql, useFragment } from 'react-relay' + +import EncodeDialogSidebar from '@/components/interfaces/encode/dialog/EncodeDialogSidebar' +import EncodeAnalyticsPanel from '@/components/interfaces/encode/dialog/panels/EncodeAnalyticsPanel' +import EncodeDialogScript from '@/components/interfaces/encode/dialog/panels/EncodeScriptPanel' +import { + EncodeDialogContext, + EncodeDialogPanel, + useEncodeDialog, +} from '@/components/interfaces/encode/dialog/use-encode-dialog.hook' + +const FRAGMENT = graphql` + fragment EncodeDialogFragment on Encode { + recipe { + name + } + file { + path_info { + name + } + } + ...EncodeScriptPanelFragment + ...EncodeAnalyticsPanelFragment + } +` + +type EncodeDialogProps = React.PropsWithChildren & { + $key: EncodeDialogFragment$key +} & DialogProps + +const EncodeDialog: React.FC = ({ $key, children, ...rest }) => { + const data = useFragment(FRAGMENT, $key) + const context = useEncodeDialog({ panel: EncodeDialogPanel.SCRIPT }) + + const content = React.useCallback(() => { + switch (context.panel) { + case EncodeDialogPanel.SCRIPT: + return + + case EncodeDialogPanel.ANALYTICS: + return + + default: + throw new Error(`unexpected panel value ${context.panel} was provided.`) + } + }, [context.panel, data]) + + return ( + + {children} + + + {({ close }) => ( + + + + + + +
+
+ {data.file.path_info.name} + + {data.recipe.name} +
+ +
+ +
+
+
+ + +
{content()}
+
+
+
+
+ )} +
+
+ ) +} + +export default EncodeDialog diff --git a/app/src/components/interfaces/encode/dialog/EncodeDialogSidebar.tsx b/app/src/components/interfaces/encode/dialog/EncodeDialogSidebar.tsx new file mode 100644 index 00000000..5847f142 --- /dev/null +++ b/app/src/components/interfaces/encode/dialog/EncodeDialogSidebar.tsx @@ -0,0 +1,28 @@ +import { Navigation } from '@giantnodes/react' +import { IconReportAnalytics, IconScript } from '@tabler/icons-react' + +import { EncodeDialogPanel, useEncodeDialogContext } from '@/components/interfaces/encode/dialog/use-encode-dialog.hook' + +const EncodeDialogSidebar = () => { + const { panel, setPanel } = useEncodeDialogContext() + + return ( + + + + setPanel(EncodeDialogPanel.SCRIPT)}> + + + + + + setPanel(EncodeDialogPanel.ANALYTICS)}> + + + + + + ) +} + +export default EncodeDialogSidebar diff --git a/app/src/components/interfaces/encode/dialog/panels/EncodeAnalyticsPanel.tsx b/app/src/components/interfaces/encode/dialog/panels/EncodeAnalyticsPanel.tsx new file mode 100644 index 00000000..2f9372fc --- /dev/null +++ b/app/src/components/interfaces/encode/dialog/panels/EncodeAnalyticsPanel.tsx @@ -0,0 +1,34 @@ +import type { EncodeAnalyticsPanelFragment$key } from '@/__generated__/EncodeAnalyticsPanelFragment.graphql' + +import { Card, Typography } from '@giantnodes/react' +import { graphql, useFragment } from 'react-relay' + +import { EncodeSizeWidget } from '@/components/interfaces/encode' + +const FRAGMENT = graphql` + fragment EncodeAnalyticsPanelFragment on Encode { + ...EncodeSizeWidgetFragment + } +` + +type EncodeAnalyticsPanelProps = { + $key: EncodeAnalyticsPanelFragment$key +} + +const EncodeAnalyticsPanel: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + return ( + + + Size + + + + + + + ) +} + +export default EncodeAnalyticsPanel diff --git a/app/src/components/interfaces/encode/dialog/panels/EncodeScriptPanel.tsx b/app/src/components/interfaces/encode/dialog/panels/EncodeScriptPanel.tsx new file mode 100644 index 00000000..7cabbd26 --- /dev/null +++ b/app/src/components/interfaces/encode/dialog/panels/EncodeScriptPanel.tsx @@ -0,0 +1,66 @@ +import type { EncodeScriptPanelFragment$key } from '@/__generated__/EncodeScriptPanelFragment.graphql' + +import { Alert, Card, Typography } from '@giantnodes/react' +import { IconAlertCircleFilled } from '@tabler/icons-react' +import { graphql, useFragment } from 'react-relay' + +import { EncodeCommandWidget, EncodeOperationWidget, EncodeOutputWidget } from '@/components/interfaces/encode' + +const FRAGMENT = graphql` + fragment EncodeScriptPanelFragment on Encode { + failure_reason + ...EncodeOperationWidgetFragment + ...EncodeCommandWidgetFragment + ...EncodeOutputWidgetFragment + } +` + +type EncodeScriptPanelProps = { + $key: EncodeScriptPanelFragment$key +} + +const EncodeScriptPanel: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + return ( + <> + {data.failure_reason && ( + + + + The encode operation encountered an error + + {data.failure_reason} + + + + )} + + + + + + + + Command + + + + + + + + + + Output + + + + + + + + ) +} + +export default EncodeScriptPanel diff --git a/app/src/components/interfaces/encode/dialog/use-encode-dialog.hook.ts b/app/src/components/interfaces/encode/dialog/use-encode-dialog.hook.ts new file mode 100644 index 00000000..8958f539 --- /dev/null +++ b/app/src/components/interfaces/encode/dialog/use-encode-dialog.hook.ts @@ -0,0 +1,30 @@ +import React from 'react' + +import { createContext } from '@/utilities/context' + +export enum EncodeDialogPanel { + SCRIPT, + ANALYTICS, +} + +type UseEncodeDialogReturn = ReturnType + +type UseEncodeDialogProps = { + panel: EncodeDialogPanel +} + +export const useEncodeDialog = (props: UseEncodeDialogProps) => { + const [panel, setPanel] = React.useState(props.panel) + + return { + panel, + setPanel, + } +} + +export const [EncodeDialogContext, useEncodeDialogContext] = createContext({ + name: 'EncodeDialogContext', + strict: true, + errorMessage: + 'useEncodeDialogContext: `context` is undefined. Seems you forgot to wrap component within ', +}) diff --git a/app/src/components/interfaces/encode/index.ts b/app/src/components/interfaces/encode/index.ts new file mode 100644 index 00000000..7680869b --- /dev/null +++ b/app/src/components/interfaces/encode/index.ts @@ -0,0 +1,11 @@ +export { default as EncodeDuration } from '@/components/interfaces/encode/chips/EncodeDuration' +export { default as EncodePercent } from '@/components/interfaces/encode/chips/EncodePercent' +export { default as EncodeSpeed } from '@/components/interfaces/encode/chips/EncodeSpeed' +export { default as EncodeStatus } from '@/components/interfaces/encode/chips/EncodeStatus' + +export { default as EncodeDialog } from '@/components/interfaces/encode/dialog/EncodeDialog' + +export { default as EncodeCommandWidget } from '@/components/interfaces/encode/widgets/EncodeCommandWidget' +export { default as EncodeOperationWidget } from '@/components/interfaces/encode/widgets/EncodeOperationWidget' +export { default as EncodeOutputWidget } from '@/components/interfaces/encode/widgets/EncodeOutputWidget' +export { default as EncodeSizeWidget } from '@/components/interfaces/encode/widgets/EncodeSizeWidget' diff --git a/app/src/components/interfaces/encode/widgets/EncodeCommandWidget.tsx b/app/src/components/interfaces/encode/widgets/EncodeCommandWidget.tsx new file mode 100644 index 00000000..887f9853 --- /dev/null +++ b/app/src/components/interfaces/encode/widgets/EncodeCommandWidget.tsx @@ -0,0 +1,23 @@ +import type { EncodeCommandWidgetFragment$key } from '@/__generated__/EncodeCommandWidgetFragment.graphql' + +import { graphql, useFragment } from 'react-relay' + +import { CodeBlock } from '@/components/ui' + +const FRAGMENT = graphql` + fragment EncodeCommandWidgetFragment on Encode { + command + } +` + +type EncodeCommandWidgetProps = { + $key: EncodeCommandWidgetFragment$key +} + +const EncodeCommandWidget: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + return {data.command} +} + +export default EncodeCommandWidget diff --git a/app/src/components/interfaces/encode/widgets/EncodeOperationWidget.tsx b/app/src/components/interfaces/encode/widgets/EncodeOperationWidget.tsx new file mode 100644 index 00000000..69fe1e91 --- /dev/null +++ b/app/src/components/interfaces/encode/widgets/EncodeOperationWidget.tsx @@ -0,0 +1,104 @@ +import type { EncodeOperationWidgetFragment$key } from '@/__generated__/EncodeOperationWidgetFragment.graphql' + +import { Chip, Table, Typography } from '@giantnodes/react' +import dayjs from 'dayjs' +import { graphql, useFragment } from 'react-relay' + +import { EncodePercent, EncodeSpeed, EncodeStatus } from '@/components/interfaces/encode' + +const FRAGMENT = graphql` + fragment EncodeOperationWidgetFragment on Encode { + id + updated_at + machine { + name + user_name + } + ...EncodePercentFragment + ...EncodeStatusFragment + ...EncodeSpeedFragment + } +` + +type EncodeOperationWidgetProps = { + $key: EncodeOperationWidgetFragment$key +} + +const EncodeOperationWidget: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + return ( + + + + name + + + value + + + + + + Status + + + + + + + + + Progress + + + + + + + + + Machine + + + {data.machine != null && ( + <> + {data.machine?.name} + {data.machine?.user_name} + + )} + + + + + + Speed + + + + + + + {data.updated_at != null && ( + + + Heartbeat + + + + + {dayjs(data.updated_at).fromNow()} + + + + + )} + +
+ ) +} + +export default EncodeOperationWidget diff --git a/app/src/components/interfaces/encode/widgets/EncodeOutputWidget.tsx b/app/src/components/interfaces/encode/widgets/EncodeOutputWidget.tsx new file mode 100644 index 00000000..e8a12a41 --- /dev/null +++ b/app/src/components/interfaces/encode/widgets/EncodeOutputWidget.tsx @@ -0,0 +1,53 @@ +import type { EncodeOutputWidgetFragment$key } from '@/__generated__/EncodeOutputWidgetFragment.graphql' +import type { EncodeOutputWidgetSubscription } from '@/__generated__/EncodeOutputWidgetSubscription.graphql' + +import React from 'react' +import { graphql, useFragment, useSubscription } from 'react-relay' + +import CodeBlock from '@/components/ui/code-block/CodeBlock' +import ScrollAnchor from '@/components/ui/ScrollAnchor' + +const FRAGMENT = graphql` + fragment EncodeOutputWidgetFragment on Encode { + id + output + } +` + +const SUBSCRIPTION = graphql` + subscription EncodeOutputWidgetSubscription($where: EncodeFilterInput) { + encode_outputted(where: $where) { + ...EncodeOutputWidgetFragment + } + } +` + +type EncodeOutputWidgetProps = { + $key: EncodeOutputWidgetFragment$key + isAnchored?: boolean +} + +const EncodeOutputWidget: React.FC = ({ $key, isAnchored }) => { + const data = useFragment(FRAGMENT, $key) + + useSubscription({ + subscription: SUBSCRIPTION, + variables: { + where: { + id: { + eq: data.id, + }, + }, + }, + }) + + const block = React.useCallback(() => {data.output}, [data.output]) + + return isAnchored ? {block()} : block() +} + +EncodeOutputWidget.defaultProps = { + isAnchored: false, +} + +export default EncodeOutputWidget diff --git a/app/src/components/interfaces/encode/widgets/EncodeSizeWidget.tsx b/app/src/components/interfaces/encode/widgets/EncodeSizeWidget.tsx new file mode 100644 index 00000000..27eff110 --- /dev/null +++ b/app/src/components/interfaces/encode/widgets/EncodeSizeWidget.tsx @@ -0,0 +1,94 @@ +import type { EncodeSizeWidgetFragment$key } from '@/__generated__/EncodeSizeWidgetFragment.graphql' +import type { DefaultTooltipContent } from 'recharts' + +import { Card, Typography } from '@giantnodes/react' +import { filesize } from 'filesize' +import React from 'react' +import { graphql, useFragment } from 'react-relay' +import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' + +const FRAGMENT = graphql` + fragment EncodeSizeWidgetFragment on Encode { + snapshots { + size + created_at + } + } +` + +type EncodeSizeWidgetProps = { + $key: EncodeSizeWidgetFragment$key +} + +const EncodeSizeWidget: React.FC = ({ $key }) => { + const data = useFragment(FRAGMENT, $key) + + const bars = React.useMemo(() => { + const snapshots = data.snapshots.toSorted((a, b) => a.created_at - b.created_at) + + const output = [ + { + name: 'Original', + size: snapshots.at(0)?.size, + fill: 'hsl(var(--twc-info) / 0.2)', + stroke: 'hsl(var(--twc-info))', + }, + { + name: 'New', + size: snapshots.length === 1 ? 0 : snapshots.at(snapshots.length - 1)?.size, + fill: 'hsl(var(--twc-brand) / 0.2)', + stroke: 'hsl(var(--twc-brand))', + }, + ] + + return output + }, [data]) + + const TooltipContent: typeof DefaultTooltipContent = React.useCallback((props) => { + const value = props.payload?.at(0)?.value + + return ( + + + {props.label} + + + {value !== undefined && ( + + + {filesize(value, { base: 2 })} + + + )} + + ) + }, []) + + return ( + + + + + + + filesize(tick, { base: 2 })} + type="number" + /> + + + + ) +} + +export default EncodeSizeWidget diff --git a/app/src/components/interfaces/explore/index.ts b/app/src/components/interfaces/explore/index.ts index b305721c..f2cbeb64 100644 --- a/app/src/components/interfaces/explore/index.ts +++ b/app/src/components/interfaces/explore/index.ts @@ -1,4 +1,3 @@ -export { default as ExploreBreadcrumbs } from '@/components/interfaces/explore/breadcrumbs/ExploreBreadcrumbs' export { default as ExploreControls } from '@/components/interfaces/explore/controls/ExploreControls' export { default as ExploreTable } from '@/components/interfaces/explore/table/ExploreTable' diff --git a/app/src/components/interfaces/explore/breadcrumbs/ExploreBreadcrumbs.tsx b/app/src/components/interfaces/file-system/FileSystemBreadcrumb.tsx similarity index 84% rename from app/src/components/interfaces/explore/breadcrumbs/ExploreBreadcrumbs.tsx rename to app/src/components/interfaces/file-system/FileSystemBreadcrumb.tsx index b217baf8..23997f26 100644 --- a/app/src/components/interfaces/explore/breadcrumbs/ExploreBreadcrumbs.tsx +++ b/app/src/components/interfaces/file-system/FileSystemBreadcrumb.tsx @@ -1,11 +1,11 @@ -import type { ExploreBreadcrumbsFragment$key } from '@/__generated__/ExploreBreadcrumbsFragment.graphql' +import type { FileSystemBreadcrumbFragment$key } from '@/__generated__/FileSystemBreadcrumbFragment.graphql' import { Breadcrumb, Link } from '@giantnodes/react' import React from 'react' import { graphql, useFragment } from 'react-relay' const FRAGMENT = graphql` - fragment ExploreBreadcrumbsFragment on FileSystemDirectory { + fragment FileSystemBreadcrumbFragment on FileSystemEntry { library { slug path_info { @@ -20,11 +20,11 @@ const FRAGMENT = graphql` } ` -type ExploreBreadcrumbsProps = { - $key: ExploreBreadcrumbsFragment$key +type FileSystemBreadcrumbProps = { + $key: FileSystemBreadcrumbFragment$key } -const ExploreBreadcrumbs: React.FC = ({ $key }) => { +const FileSystemBreadcrumb: React.FC = ({ $key }) => { const data = useFragment(FRAGMENT, $key) const directories = React.useMemo>(() => { @@ -73,4 +73,4 @@ const ExploreBreadcrumbs: React.FC = ({ $key }) => { ) } -export default ExploreBreadcrumbs +export default FileSystemBreadcrumb diff --git a/app/src/components/interfaces/file-system/index.ts b/app/src/components/interfaces/file-system/index.ts new file mode 100644 index 00000000..0ac04af8 --- /dev/null +++ b/app/src/components/interfaces/file-system/index.ts @@ -0,0 +1 @@ +export { default as FileSystemBreadcrumb } from '@/components/interfaces/file-system/FileSystemBreadcrumb' diff --git a/app/src/components/interfaces/index.ts b/app/src/components/interfaces/index.ts deleted file mode 100644 index 93b310ed..00000000 --- a/app/src/components/interfaces/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@/components/interfaces/explore' diff --git a/app/src/components/layouts/dashboard/navbar/Navbar.tsx b/app/src/components/layouts/dashboard/navbar/Navbar.tsx index e38bb23e..ce858bdd 100644 --- a/app/src/components/layouts/dashboard/navbar/Navbar.tsx +++ b/app/src/components/layouts/dashboard/navbar/Navbar.tsx @@ -4,7 +4,7 @@ import { Input, Navigation } from '@giantnodes/react' import { IconBell, IconSearch } from '@tabler/icons-react' const Navbar: React.FC = (props) => ( - + diff --git a/app/src/components/layouts/dashboard/sidebar/SettingsSidebar.tsx b/app/src/components/layouts/dashboard/sidebar/SettingsSidebar.tsx index 37097b20..6a5e6c06 100644 --- a/app/src/components/layouts/dashboard/sidebar/SettingsSidebar.tsx +++ b/app/src/components/layouts/dashboard/sidebar/SettingsSidebar.tsx @@ -2,7 +2,6 @@ import { Navigation } from '@giantnodes/react' import { IconHomeCog, IconServerCog, IconUserCog } from '@tabler/icons-react' -import Link from 'next/link' import { usePathname } from 'next/navigation' const SettingSidebar: React.FC = () => { @@ -11,36 +10,30 @@ const SettingSidebar: React.FC = () => { const route = router.split('/')[2] return ( - + Settings - - - - General - - + + + General + - - - - - Preferences - - + + + + Preferences + - - - - - Encoder - - + + + + Encoder + diff --git a/app/src/components/layouts/dashboard/sidebar/Sidebar.tsx b/app/src/components/layouts/dashboard/sidebar/Sidebar.tsx index 12cdfd45..6587e09b 100644 --- a/app/src/components/layouts/dashboard/sidebar/Sidebar.tsx +++ b/app/src/components/layouts/dashboard/sidebar/Sidebar.tsx @@ -35,20 +35,20 @@ const Sidebar: React.FC = ({ $key, ...rest }) => { const route = router.split('/')[1] return ( - + giantnodes logo - - + + Dashboard - - + + Recipes @@ -68,8 +68,8 @@ const Sidebar: React.FC = ({ $key, ...rest }) => { - - + + Settings diff --git a/app/src/components/layouts/dashboard/sidebar/SidebarLibrarySegment.tsx b/app/src/components/layouts/dashboard/sidebar/SidebarLibrarySegment.tsx index 74157925..89dd127c 100644 --- a/app/src/components/layouts/dashboard/sidebar/SidebarLibrarySegment.tsx +++ b/app/src/components/layouts/dashboard/sidebar/SidebarLibrarySegment.tsx @@ -9,7 +9,6 @@ import type { AvatarProps } from '@giantnodes/react' import { Avatar, Navigation } from '@giantnodes/react' import { IconFolderCheck, IconFolderExclamation, IconFolderQuestion, IconFolderX } from '@tabler/icons-react' -import Link from 'next/link' import { graphql, usePaginationFragment } from 'react-relay' const FRAGMENT = graphql` @@ -78,16 +77,14 @@ const SidebarLibrarySegment: React.FC = ({ $key }) = <> {data?.libraries?.edges?.map((edge) => ( - - - - - - + + + + + - {edge.node.name} - - + {edge.node.name} + ))} diff --git a/app/src/components/layouts/library/navbar/Navbar.tsx b/app/src/components/layouts/library/navbar/Navbar.tsx index e11256a2..385edb7d 100644 --- a/app/src/components/layouts/library/navbar/Navbar.tsx +++ b/app/src/components/layouts/library/navbar/Navbar.tsx @@ -6,7 +6,7 @@ import { Input, Navigation } from '@giantnodes/react' import { IconBell, IconSearch } from '@tabler/icons-react' const Navbar: React.FC = (props) => ( - + diff --git a/app/src/components/layouts/library/sidebar/SettingSidebar.tsx b/app/src/components/layouts/library/sidebar/SettingSidebar.tsx index d8457ea1..f3e34554 100644 --- a/app/src/components/layouts/library/sidebar/SettingSidebar.tsx +++ b/app/src/components/layouts/library/sidebar/SettingSidebar.tsx @@ -2,7 +2,6 @@ import { Navigation } from '@giantnodes/react' import { IconHomeCog } from '@tabler/icons-react' -import Link from 'next/link' import { usePathname } from 'next/navigation' import { useLibraryContext } from '@/app/(libraries)/library/[slug]/use-library.hook' @@ -14,19 +13,17 @@ const SettingSidebar: React.FC = () => { const route = router.split('/')[4] return ( - + Settings - - - - - General - - + + + + General + diff --git a/app/src/components/layouts/library/sidebar/Sidebar.tsx b/app/src/components/layouts/library/sidebar/Sidebar.tsx index b833c325..e31cf034 100644 --- a/app/src/components/layouts/library/sidebar/Sidebar.tsx +++ b/app/src/components/layouts/library/sidebar/Sidebar.tsx @@ -5,7 +5,6 @@ import type { NavigationProps } from '@giantnodes/react' import { Navigation } from '@giantnodes/react' import { IconFolders, IconGauge, IconHome, IconSettings } from '@tabler/icons-react' import Image from 'next/image' -import Link from 'next/link' import { usePathname } from 'next/navigation' import { useLibraryContext } from '@/app/(libraries)/library/[slug]/use-library.hook' @@ -17,46 +16,38 @@ const Sidebar: React.FC = ({ ...rest }) => { const route = router.split('/')[3] return ( - + giantnodes logo - - - - - + + + - - - - - - + + + + - - - - - - + + + + - - - - - - + + + + diff --git a/app/src/components/tables/encoded/EncodedTable.tsx b/app/src/components/tables/encoded/EncodedTable.tsx index 8e56e7e3..7dadbc31 100644 --- a/app/src/components/tables/encoded/EncodedTable.tsx +++ b/app/src/components/tables/encoded/EncodedTable.tsx @@ -1,12 +1,10 @@ import type { EncodedTableFragment$key } from '@/__generated__/EncodedTableFragment.graphql' import type { EncodedTableRefetchQuery } from '@/__generated__/EncodedTableRefetchQuery.graphql' -import { Button, Table, Typography } from '@giantnodes/react' +import { Button, Link, Table } from '@giantnodes/react' import React from 'react' import { graphql, usePaginationFragment } from 'react-relay' -import { EncodeBadges } from '@/components/ui' - const FRAGMENT = graphql` fragment EncodedTableFragment on Query @refetchable(queryName: "EncodedTableRefetchQuery") @@ -26,7 +24,6 @@ const FRAGMENT = graphql` name } } - ...EncodeBadgesFragment } } pageInfo { @@ -60,11 +57,9 @@ const EncodedTable: React.FC = ({ $key }) => { {(item) => ( - {item.node.file.path_info.name} - - - + {item.node.file.path_info.name} + Tbd )} diff --git a/app/src/components/tables/encoding/EncodingTable.tsx b/app/src/components/tables/encoding/EncodingTable.tsx index 95c6fced..855cdab7 100644 --- a/app/src/components/tables/encoding/EncodingTable.tsx +++ b/app/src/components/tables/encoding/EncodingTable.tsx @@ -5,12 +5,10 @@ import type { } from '@/__generated__/EncodingTableFragment.graphql' import type { EncodingTableRefetchQuery } from '@/__generated__/EncodingTableRefetchQuery.graphql' -import { Button, Table, Typography } from '@giantnodes/react' +import { Button, Link, Table } from '@giantnodes/react' import { IconProgressX } from '@tabler/icons-react' import React from 'react' -import { graphql, useMutation, usePaginationFragment, useSubscription } from 'react-relay' - -import { EncodeBadges } from '@/components/ui' +import { graphql, useMutation, usePaginationFragment } from 'react-relay' const FRAGMENT = graphql` fragment EncodingTableFragment on Query @@ -31,7 +29,6 @@ const FRAGMENT = graphql` name } } - ...EncodeBadgesFragment } } pageInfo { @@ -59,19 +56,6 @@ const MUTATION = graphql` } ` -const SUBSCRIPTION = graphql` - subscription EncodingTableSubscription { - encode_speed_change { - percent - speed { - frames - bitrate - scale - } - } - } -` - type EncodeEntry = NonNullable['edges']>[0]['node'] type EncodingTableProps = { @@ -86,11 +70,6 @@ const EncodingTable: React.FC = ({ $key }) => { const [commit] = useMutation(MUTATION) - useSubscription({ - subscription: SUBSCRIPTION, - variables: {}, - }) - const cancel = React.useCallback( (entry: EncodeEntry) => { commit({ @@ -118,12 +97,10 @@ const EncodingTable: React.FC = ({ $key }) => { {(item) => ( - {item.node.file.path_info.name} + {item.node.file.path_info.name}
- - diff --git a/app/src/components/ui/ScrollAnchor.tsx b/app/src/components/ui/ScrollAnchor.tsx new file mode 100644 index 00000000..972c08ea --- /dev/null +++ b/app/src/components/ui/ScrollAnchor.tsx @@ -0,0 +1,18 @@ +import React from 'react' + +type ScrollAnchorProps = React.PropsWithChildren + +const ScrollAnchor: React.FC = ({ children }) => { + const ref = React.useRef(null) + + React.useEffect(() => ref.current?.scrollIntoView({ behavior: 'smooth' })) + + return ( + <> + {children} +
+ + ) +} + +export default ScrollAnchor diff --git a/app/src/components/ui/code-block/CodeBlock.tsx b/app/src/components/ui/code-block/CodeBlock.tsx new file mode 100644 index 00000000..ec6c4c81 --- /dev/null +++ b/app/src/components/ui/code-block/CodeBlock.tsx @@ -0,0 +1,22 @@ +import type { SyntaxHighlighterProps } from 'react-syntax-highlighter' + +import SyntaxHighlighter from 'react-syntax-highlighter' +import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs' + +type CodeBlockProps = Pick & { + children?: string | string[] | null +} + +const CodeBlock: React.FC = ({ children, ...rest }) => ( + + {children ?? ''} + +) + +export default CodeBlock diff --git a/app/src/components/ui/encode-badges/EncodeBadges.tsx b/app/src/components/ui/encode-badges/EncodeBadges.tsx deleted file mode 100644 index e85c4515..00000000 --- a/app/src/components/ui/encode-badges/EncodeBadges.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import type { EncodeBadgesFragment$key, EncodeStatus } from '@/__generated__/EncodeBadgesFragment.graphql' -import type { ChipProps } from '@giantnodes/react' - -import { Chip } from '@giantnodes/react' -import { IconArrowRight, IconTrendingDown, IconTrendingUp } from '@tabler/icons-react' -import dayjs from 'dayjs' -import { filesize } from 'filesize' -import React from 'react' -import { graphql, useFragment } from 'react-relay' - -type EncodeBadgesProps = Omit & { - $key: EncodeBadgesFragment$key -} - -const FRAGMENT = graphql` - fragment EncodeBadgesFragment on Encode { - status - percent - started_at - failed_at - cancelled_at - completed_at - created_at - speed { - frames - bitrate - scale - } - snapshots { - size - probed_at - } - } -` - -const EncodeBadges: React.FC = ({ $key, size }) => { - const data = useFragment(FRAGMENT, $key) - - const percent = (value: number): string => - Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 2, - }).format(value) - - const getStatusColour = (status: EncodeStatus) => { - switch (status) { - case 'SUBMITTED': - return 'info' - - case 'QUEUED': - return 'info' - - case 'ENCODING': - return 'success' - - case 'DEGRADED': - return 'warning' - - case 'COMPLETED': - return 'success' - - case 'CANCELLED': - return 'neutral' - - case 'FAILED': - return 'danger' - - default: - return 'neutral' - } - } - - const SizeChip = React.useCallback(() => { - const difference = data.snapshots[data.snapshots.length - 1].size - data.snapshots[0].size - const increase = Math.abs(difference / data.snapshots[0].size) - - const icon = () => { - switch (true) { - case increase > 0: - return - - case increase < 0: - return - - case increase === 0: - default: - return - } - } - - const color = () => { - switch (true) { - case increase > 0: - return 'danger' - - case increase < 0: - return 'success' - - case increase === 0: - default: - return 'info' - } - } - - return ( - - {icon()} - - {percent(increase)} - - ) - }, [data.snapshots, size]) - - return ( -
- {data.status.toLowerCase()} - - {data.status !== 'COMPLETED' && data.status !== 'CANCELLED' && data.status !== 'FAILED' && ( - <> - {data.speed != null && ( - <> - - {data.speed.frames} fps - - - - {filesize(data.speed.bitrate * 0.125, { bits: true }).toLowerCase()}/s - - - - {data.speed.scale.toFixed(2)}x - - - )} - - {data.percent != null && ( - - {percent(data.percent)} - - )} - - )} - - {data.status === 'COMPLETED' && ( - <> - - {dayjs.duration(dayjs(data.completed_at).diff(data.created_at)).format('H[h] m[m] s[s]')} - - - {SizeChip()} - - )} - - {data.status === 'CANCELLED' && ( - - {dayjs.duration(dayjs(data.cancelled_at).diff(data.created_at)).format('H[h] m[m] s[s]')} - - )} - - {data.status === 'FAILED' && ( - - {dayjs.duration(dayjs(data.failed_at).diff(data.created_at)).format('H[h] m[m] s[s]')} - - )} -
- ) -} - -export default EncodeBadges diff --git a/app/src/components/ui/index.ts b/app/src/components/ui/index.ts index 8626f0ed..cb91940b 100644 --- a/app/src/components/ui/index.ts +++ b/app/src/components/ui/index.ts @@ -1 +1 @@ -export { default as EncodeBadges } from '@/components/ui/encode-badges/EncodeBadges' +export { default as CodeBlock } from '@/components/ui/code-block/CodeBlock' diff --git a/app/src/libraries/dayjs/index.ts b/app/src/libraries/dayjs/index.ts index e4ef58e5..61eb3d04 100644 --- a/app/src/libraries/dayjs/index.ts +++ b/app/src/libraries/dayjs/index.ts @@ -1,6 +1,8 @@ import dayjs from 'dayjs' import duration from 'dayjs/plugin/duration' +import localized from 'dayjs/plugin/localizedFormat' import relative from 'dayjs/plugin/relativeTime' dayjs.extend(duration) +dayjs.extend(localized) dayjs.extend(relative) diff --git a/app/src/styles/global.css b/app/src/styles/global.css index f154d2c6..eff9f771 100644 --- a/app/src/styles/global.css +++ b/app/src/styles/global.css @@ -2,22 +2,21 @@ @tailwind components; @tailwind utilities; -@layer base { - :root { - --color-backdrop: 250 250 250; - --color-background: 255 255 255; - --color-middleground: 250 250 250; - --color-foreground: 212 212 216; - --color-title: 39 39 42; - --color-subtitle: 82 82 91; - } +::-webkit-scrollbar { + width: 20px; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: theme('colors.partition'); + border-radius: theme('borderRadius.xl'); + border: 5px solid transparent; + background-clip: content-box; +} - :root[class~='dark'] { - --color-backdrop: 18 18 18; - --color-background: 24 24 27; - --color-middleground: 39 39 42; - --color-foreground: 63 63 70; - --color-title: 244 244 245; - --color-subtitle: 161 161 170; - } +::-webkit-scrollbar-thumb:hover { + background-color: theme('colors.foreground'); } diff --git a/app/src/utilities/numbers.ts b/app/src/utilities/numbers.ts new file mode 100644 index 00000000..10407dc6 --- /dev/null +++ b/app/src/utilities/numbers.ts @@ -0,0 +1,5 @@ +export const percent = (value: number): string => + Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 2, + }).format(value) diff --git a/src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeOutputtedTopic.cs b/src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeOutputtedTopic.cs new file mode 100644 index 00000000..f9a51fe2 --- /dev/null +++ b/src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeOutputtedTopic.cs @@ -0,0 +1,25 @@ +using Giantnodes.Service.Dashboard.Application.Contracts.Encodes.Events; +using Giantnodes.Service.Dashboard.Domain.Aggregates.Encodes.Repositories; +using HotChocolate.Subscriptions; +using MassTransit; + +namespace Giantnodes.Service.Dashboard.Application.Components.Encodes.Events; + +public class RaiseEncodeOutputtedTopic : IConsumer +{ + private readonly IEncodeRepository _repository; + private readonly ITopicEventSender _sender; + + public RaiseEncodeOutputtedTopic(IEncodeRepository repository, ITopicEventSender sender) + { + _repository = repository; + _sender = sender; + } + + public async Task Consume(ConsumeContext context) + { + var encode = await _repository.SingleAsync(x => x.Id == context.Message.EncodeId); + + await _sender.SendAsync(nameof(EncodeOutputtedEvent), encode, context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeProgressedTopic.cs b/src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeProgressedTopic.cs new file mode 100644 index 00000000..70a10dfd --- /dev/null +++ b/src/Service.Dashboard/src/Application.Components/Encodes/Events/RaiseEncodeProgressedTopic.cs @@ -0,0 +1,25 @@ +using Giantnodes.Service.Dashboard.Application.Contracts.Encodes.Events; +using Giantnodes.Service.Dashboard.Domain.Aggregates.Encodes.Repositories; +using HotChocolate.Subscriptions; +using MassTransit; + +namespace Giantnodes.Service.Dashboard.Application.Components.Encodes.Events; + +public class RaiseEncodeProgressedTopic : IConsumer +{ + private readonly IEncodeRepository _repository; + private readonly ITopicEventSender _sender; + + public RaiseEncodeProgressedTopic(IEncodeRepository repository, ITopicEventSender sender) + { + _repository = repository; + _sender = sender; + } + + public async Task Consume(ConsumeContext context) + { + var encode = await _repository.SingleAsync(x => x.Id == context.Message.EncodeId); + + await _sender.SendAsync(nameof(EncodeProgressedEvent), encode, context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/Activities/EncodeOperationOutputtedDataActivity.cs b/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/Activities/EncodeOperationOutputtedDataActivity.cs new file mode 100644 index 00000000..ca89f786 --- /dev/null +++ b/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/Activities/EncodeOperationOutputtedDataActivity.cs @@ -0,0 +1,50 @@ +using Giantnodes.Infrastructure.Uow.Services; +using Giantnodes.Service.Dashboard.Domain.Aggregates.Encodes.Repositories; +using Giantnodes.Service.Dashboard.Persistence.Sagas; +using Giantnodes.Service.Encoder.Application.Contracts.Encoding.Events; +using MassTransit; + +namespace Giantnodes.Service.Dashboard.Application.Components.Encodes.Sagas.Activities; + +public class EncodeOperationOutputtedDataActivity : IStateMachineActivity +{ + private readonly IUnitOfWorkService _uow; + private readonly IEncodeRepository _repository; + + public EncodeOperationOutputtedDataActivity(IUnitOfWorkService uow, IEncodeRepository repository) + { + _uow = uow; + _repository = repository; + } + + public void Probe(ProbeContext context) + { + context.CreateScope(KebabCaseEndpointNameFormatter.Instance.Message()); + } + + public void Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } + + public async Task Execute( + BehaviorContext context, + IBehavior next) + { + using var uow = await _uow.BeginAsync(context.CancellationToken); + var encode = await _repository.SingleAsync(x => x.Id == context.Saga.EncodeId); + + encode.AppendOutputLog(context.Message.Data); + + await uow.CommitAsync(context.CancellationToken); + await next.Execute(context); + } + + public Task Faulted( + BehaviorExceptionContext context, + IBehavior next) + where TException : Exception + { + return next.Faulted(context); + } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachine.cs b/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachine.cs index a7fe68db..2884f019 100644 --- a/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachine.cs +++ b/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachine.cs @@ -21,6 +21,7 @@ public EncodeStateMachine() Event(() => Built); Event(() => Heartbeat); Event(() => Progressed); + Event(() => Outputted); Event(() => Completed); Event(() => Failed); @@ -73,6 +74,8 @@ public EncodeStateMachine() .Finalize()); DuringAny( + When(Outputted) + .Activity(context => context.OfType()), When(Failed) .Then(context => context.Saga.FailedReason = context.Message.Exceptions.Message) .Activity(context => context.OfInstanceType()) @@ -95,6 +98,7 @@ public EncodeStateMachine() public required Event Built { get; set; } public required Event Heartbeat { get; set; } public required Event Progressed { get; set; } + public required Event Outputted { get; set; } public required Event Completed { get; set; } public required Event Cancelled { get; set; } public required Event Failed { get; set; } diff --git a/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachineDefinition.cs b/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachineDefinition.cs index 7d84e7e0..a4250d3d 100644 --- a/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachineDefinition.cs +++ b/src/Service.Dashboard/src/Application.Components/Encodes/Sagas/EncodeStateMachineDefinition.cs @@ -1,5 +1,5 @@ -using Giantnodes.Service.Dashboard.Persistence.DbContexts; using Giantnodes.Service.Dashboard.Persistence.Sagas; +using Giantnodes.Service.Encoder.Application.Contracts.Encoding.Events; using MassTransit; namespace Giantnodes.Service.Dashboard.Application.Components.Encodes.Sagas; @@ -11,8 +11,11 @@ protected override void ConfigureSaga( ISagaConfigurator sagaConfigurator, IRegistrationContext context) { - endpointConfigurator.ConcurrentMessageLimit = 3; - endpointConfigurator.UseMessageRetry(r => r.Interval(3, TimeSpan.FromSeconds(3))); + + var partition = sagaConfigurator.CreatePartitioner(1); + endpointConfigurator.UsePartitioner(partition, p => p.Message.JobId); + endpointConfigurator.UsePartitioner(partition, p => p.Message.JobId); + endpointConfigurator.UsePartitioner(partition, p => p.Message.JobId); } } \ No newline at end of file diff --git a/src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeOutputtedEvent.cs b/src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeOutputtedEvent.cs new file mode 100644 index 00000000..df858923 --- /dev/null +++ b/src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeOutputtedEvent.cs @@ -0,0 +1,12 @@ +using Giantnodes.Infrastructure.Domain.Events; + +namespace Giantnodes.Service.Dashboard.Application.Contracts.Encodes.Events; + +public sealed record EncodeOutputtedEvent : DomainEvent +{ + public required Guid EncodeId { get; init; } + + public required string Output { get; init; } + + public required string FullOutput { get; init; } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeProgressedEvent.cs b/src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeProgressedEvent.cs new file mode 100644 index 00000000..ecde7fb2 --- /dev/null +++ b/src/Service.Dashboard/src/Application.Contracts/Encodes/Events/EncodeProgressedEvent.cs @@ -0,0 +1,10 @@ +using Giantnodes.Infrastructure.Domain.Events; + +namespace Giantnodes.Service.Dashboard.Application.Contracts.Encodes.Events; + +public sealed record EncodeProgressedEvent : DomainEvent +{ + public required Guid EncodeId { get; init; } + + public required float Percent { get; set; } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/Domain/Aggregates/Encodes/Encode.cs b/src/Service.Dashboard/src/Domain/Aggregates/Encodes/Encode.cs index c2a7ab2d..f9a764c8 100644 --- a/src/Service.Dashboard/src/Domain/Aggregates/Encodes/Encode.cs +++ b/src/Service.Dashboard/src/Domain/Aggregates/Encodes/Encode.cs @@ -16,36 +16,85 @@ public class Encode : AggregateRoot, ITimestampableEntity { private readonly List _snapshots = new(); + /// + /// The file being encoded. + /// public FileSystemFile File { get; private set; } + /// + /// The recipe used for encoding. + /// public Recipe Recipe { get; private set; } + /// + /// The current encoding speed. + /// public EncodeSpeed? Speed { get; private set; } + /// + /// The current status of the encoding process. + /// public EncodeStatus Status { get; private set; } + /// + /// The machine performing the encoding. + /// + public Machine? Machine { get; private set; } + + /// + /// The current progress percentage of the encoding process. + /// public float? Percent { get; private set; } - public string? FfmpegCommand { get; private set; } + /// + /// The ffmpeg command used for the encoding. + /// + public string? Command { get; private set; } - public Machine? Machine { get; private set; } + /// + /// The ffmpeg output log of the encoding process. + /// + public string? Output { get; private set; } + /// + /// The timestamp when the encoding started. + /// public DateTime? StartedAt { get; private set; } + /// + /// The timestamp when the encoding failed. + /// public DateTime? FailedAt { get; private set; } + /// + /// The reason for the encoding failure. + /// public string? FailureReason { get; private set; } + /// + /// The timestamp when the encoding was degraded. + /// public DateTime? DegradedAt { get; private set; } + /// + /// The timestamp when the encoding was cancelled. + /// public DateTime? CancelledAt { get; private set; } + /// + /// The timestamp when the encoding was completed. + /// public DateTime? CompletedAt { get; private set; } + /// public DateTime CreatedAt { get; private set; } + /// public DateTime? UpdatedAt { get; private set; } + /// + /// A collection of snapshots taken during the encoding process. + /// public IReadOnlyCollection Snapshots { get; private set; } private Encode() @@ -113,6 +162,10 @@ public void SetCancelled(DateTime when) DomainEvents.Add(new EncodeCancelledEvent { EncodeId = Id }); } + /// + /// Sets the progress of the encoding process. + /// + /// The current progress percentage. public void SetProgress(float progress) { if (Status is not EncodeStatus.Encoding) @@ -121,8 +174,18 @@ public void SetProgress(float progress) Guard.Against.OutOfRange(progress, nameof(progress), 0, 1); Percent = progress; + + DomainEvents.Add(new EncodeProgressedEvent + { + EncodeId = Id, + Percent = Percent.Value + }); } + /// + /// Sets the encoding speed. + /// + /// The encoding speed. public void SetSpeed(EncodeSpeed speed) { if (Status is not EncodeStatus.Encoding) @@ -139,14 +202,41 @@ public void SetSpeed(EncodeSpeed speed) }); } + /// + /// Sets the ffmpeg conversion command and the machine performing the encoding. + /// + /// The machine performing the encoding. + /// The ffmpeg command for the encoding. public void SetFfmpegConversion(Machine machine, string command) { Guard.Against.NullOrWhiteSpace(command); Machine = machine; - FfmpegCommand = command; + Command = command; + } + + /// + /// Appends the given ffmpeg output to the encoding log. + /// + /// The ffmpeg output to be appended. + public void AppendOutputLog(string output) + { + Guard.Against.NullOrWhiteSpace(output); + + Output = string.Join(Environment.NewLine, Output, output); + + DomainEvents.Add(new EncodeOutputtedEvent + { + EncodeId = Id, + Output = output, + FullOutput = Output + }); } + /// + /// Adds a snapshot taken during the encoding process. + /// + /// The snapshot to be added. public void AddSnapshot(EncodeSnapshot snapshot) { _snapshots.Add(snapshot); diff --git a/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Queries/EncodeFindOne.cs b/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Queries/EncodeFindOne.cs new file mode 100644 index 00000000..ea8afadc --- /dev/null +++ b/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Queries/EncodeFindOne.cs @@ -0,0 +1,18 @@ +using Giantnodes.Service.Dashboard.Domain.Aggregates.Encodes; +using Giantnodes.Service.Dashboard.Persistence.DbContexts; +using Microsoft.EntityFrameworkCore; + +namespace Giantnodes.Service.Dashboard.HttpApi.Resolvers.Encodes.Queries; + +[ExtendObjectType(OperationTypeNames.Query)] +public class EncodeFindOne +{ + [UseFirstOrDefault] + [UseProjection] + [UseFiltering] + [UseSorting] + public IQueryable Encode([Service] ApplicationDbContext database) + { + return database.Encodes.AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeOutputtedSubscription.cs b/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeOutputtedSubscription.cs new file mode 100644 index 00000000..46221f30 --- /dev/null +++ b/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeOutputtedSubscription.cs @@ -0,0 +1,23 @@ +using Giantnodes.Service.Dashboard.Application.Contracts.Encodes.Events; +using Giantnodes.Service.Dashboard.Domain.Aggregates.Encodes; +using Giantnodes.Service.Dashboard.Persistence.DbContexts; +using Microsoft.EntityFrameworkCore; + +namespace Giantnodes.Service.Dashboard.HttpApi.Resolvers.Encodes.Subscriptions; + +[ExtendObjectType(OperationTypeNames.Subscription)] +public class EncodeOutputtedSubscription +{ + [Subscribe] + [Topic(nameof(EncodeOutputtedEvent))] + [UseSingleOrDefault] + [UseProjection] + [UseFiltering] + [UseSorting] + public IQueryable EncodeOutputted( + [Service] ApplicationDbContext database, + [EventMessage] Encode encode) + { + return database.Encodes.Where(x => x.Id == encode.Id).AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeProgressedSubscription.cs b/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeProgressedSubscription.cs new file mode 100644 index 00000000..0cb54f81 --- /dev/null +++ b/src/Service.Dashboard/src/HttpApi/Resolvers/Encodes/Subscriptions/EncodeProgressedSubscription.cs @@ -0,0 +1,23 @@ +using Giantnodes.Service.Dashboard.Application.Contracts.Encodes.Events; +using Giantnodes.Service.Dashboard.Domain.Aggregates.Encodes; +using Giantnodes.Service.Dashboard.Persistence.DbContexts; +using Microsoft.EntityFrameworkCore; + +namespace Giantnodes.Service.Dashboard.HttpApi.Resolvers.Encodes.Subscriptions; + +[ExtendObjectType(OperationTypeNames.Subscription)] +public class EncodeProgressedSubscription +{ + [Subscribe] + [Topic(nameof(EncodeProgressedEvent))] + [UseSingleOrDefault] + [UseProjection] + [UseFiltering] + [UseSorting] + public IQueryable EncodeProgressed( + [Service] ApplicationDbContext database, + [EventMessage] Encode encode) + { + return database.Encodes.Where(x => x.Id == encode.Id).AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/HttpApi/Types/Encodes/Filters/EncodeFilterType.cs b/src/Service.Dashboard/src/HttpApi/Types/Encodes/Filters/EncodeFilterType.cs new file mode 100644 index 00000000..aba98bbb --- /dev/null +++ b/src/Service.Dashboard/src/HttpApi/Types/Encodes/Filters/EncodeFilterType.cs @@ -0,0 +1,14 @@ +using Giantnodes.Service.Dashboard.Domain.Aggregates.Encodes; +using HotChocolate.Data.Filters; + +namespace Giantnodes.Service.Dashboard.HttpApi.Types.Encodes.Filters; + +public class EncodeFilterType : FilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor + .Field(p => p.Id) + .Type(); + } +} \ No newline at end of file diff --git a/src/Service.Dashboard/src/HttpApi/Types/Encodes/Objects/EncodeType.cs b/src/Service.Dashboard/src/HttpApi/Types/Encodes/Objects/EncodeType.cs index f46af258..834512b2 100644 --- a/src/Service.Dashboard/src/HttpApi/Types/Encodes/Objects/EncodeType.cs +++ b/src/Service.Dashboard/src/HttpApi/Types/Encodes/Objects/EncodeType.cs @@ -17,6 +17,9 @@ protected override void Configure(IObjectTypeDescriptor descriptor) descriptor .Field(p => p.File); + descriptor + .Field(p => p.Recipe); + descriptor .Field(p => p.Status); @@ -27,7 +30,10 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .Field(p => p.Speed); descriptor - .Field(p => p.FfmpegCommand); + .Field(p => p.Command); + + descriptor + .Field(p => p.Output); descriptor .Field(p => p.Machine); diff --git a/src/Service.Encoder/src/Application.Components/Encoding/Jobs/EncodeFileConsumer.cs b/src/Service.Encoder/src/Application.Components/Encoding/Jobs/EncodeFileConsumer.cs index adb729a9..5329907d 100644 --- a/src/Service.Encoder/src/Application.Components/Encoding/Jobs/EncodeFileConsumer.cs +++ b/src/Service.Encoder/src/Application.Components/Encoding/Jobs/EncodeFileConsumer.cs @@ -86,6 +86,21 @@ public async Task Run(JobContext context) } }; + conversion.OnDataReceived += async (_, args) => + { + if (string.IsNullOrWhiteSpace(args.Data)) + return; + + var @event = new EncodeOperationOutputtedEvent + { + JobId = context.JobId, + CorrelationId = context.Job.CorrelationId, + Data = args.Data + }; + + await context.Publish(@event, context.CancellationToken); + }; + ConversionProgressEventArgs? progress = null; conversion.OnProgress += async (_, args) => { diff --git a/src/Service.Encoder/src/Application.Contracts/Encoding/Events/EncodeOperationOutputtedEvent.cs b/src/Service.Encoder/src/Application.Contracts/Encoding/Events/EncodeOperationOutputtedEvent.cs new file mode 100644 index 00000000..37838555 --- /dev/null +++ b/src/Service.Encoder/src/Application.Contracts/Encoding/Events/EncodeOperationOutputtedEvent.cs @@ -0,0 +1,10 @@ +using Giantnodes.Infrastructure.Domain.Events; + +namespace Giantnodes.Service.Encoder.Application.Contracts.Encoding.Events; + +public sealed record EncodeOperationOutputtedEvent : IntegrationEvent +{ + public required Guid JobId { get; init; } + + public required string Data { get; init; } +} \ No newline at end of file