From 747254603604aaeb57e9ce5ff09ec7f6f1fbefdc Mon Sep 17 00:00:00 2001 From: Pierre Chalamet Date: Fri, 16 Jan 2026 11:01:34 +0100 Subject: [PATCH] cleanup ui implementation --- src/Terrabuild.UI/package.json | 6 +- src/Terrabuild.UI/pnpm-lock.yaml | 283 ++---- src/Terrabuild.UI/src/App.tsx | 939 ++++++------------ .../src/components/BuildControlsPanel.tsx | 243 +++++ .../src/components/BuildDetailsPanel.tsx | 78 ++ .../src/components/BuildLogPanel.tsx | 63 ++ .../src/components/GraphPanel.tsx | 109 ++ .../src/components/NodeDetailsPanel.tsx | 61 ++ .../src/components/SidebarHeader.tsx | 63 ++ .../src/components/ThemeSwitcher.tsx | 39 + src/Terrabuild.UI/src/main.tsx | 64 +- src/Terrabuild.UI/src/styles.css | 20 + src/Terrabuild.UI/src/types.ts | 58 ++ 13 files changed, 1123 insertions(+), 903 deletions(-) create mode 100644 src/Terrabuild.UI/src/components/BuildControlsPanel.tsx create mode 100644 src/Terrabuild.UI/src/components/BuildDetailsPanel.tsx create mode 100644 src/Terrabuild.UI/src/components/BuildLogPanel.tsx create mode 100644 src/Terrabuild.UI/src/components/GraphPanel.tsx create mode 100644 src/Terrabuild.UI/src/components/NodeDetailsPanel.tsx create mode 100644 src/Terrabuild.UI/src/components/SidebarHeader.tsx create mode 100644 src/Terrabuild.UI/src/components/ThemeSwitcher.tsx create mode 100644 src/Terrabuild.UI/src/types.ts diff --git a/src/Terrabuild.UI/package.json b/src/Terrabuild.UI/package.json index 10bde0af..0610b43a 100644 --- a/src/Terrabuild.UI/package.json +++ b/src/Terrabuild.UI/package.json @@ -12,9 +12,9 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mantine/core": "^6.0.0", - "@mantine/hooks": "^6.0.0", - "@mantine/notifications": "^6.0.0", + "@mantine/core": "^8.0.0", + "@mantine/hooks": "^8.0.0", + "@mantine/notifications": "^8.0.0", "@tabler/icons-react": "^3.19.0", "dagre": "^0.8.5", "react": "^18.3.1", diff --git a/src/Terrabuild.UI/pnpm-lock.yaml b/src/Terrabuild.UI/pnpm-lock.yaml index 2120e140..80bc2d57 100644 --- a/src/Terrabuild.UI/pnpm-lock.yaml +++ b/src/Terrabuild.UI/pnpm-lock.yaml @@ -15,14 +15,14 @@ importers: specifier: ^11.11.5 version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(@types/react@18.3.27)(react@18.3.1) '@mantine/core': - specifier: ^6.0.0 - version: 6.0.22(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(@mantine/hooks@6.0.22(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^8.0.0 + version: 8.3.12(@mantine/hooks@8.3.12(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/hooks': - specifier: ^6.0.0 - version: 6.0.22(react@18.3.1) + specifier: ^8.0.0 + version: 8.3.12(react@18.3.1) '@mantine/notifications': - specifier: ^6.0.0 - version: 6.0.22(@mantine/core@6.0.22(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(@mantine/hooks@6.0.22(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@6.0.22(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^8.0.0 + version: 8.3.12(@mantine/core@8.3.12(@mantine/hooks@8.3.12(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.12(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tabler/icons-react': specifier: ^3.19.0 version: 3.36.1(react@18.3.1) @@ -348,17 +348,17 @@ packages: '@floating-ui/dom@1.7.4': resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - '@floating-ui/react-dom@1.3.0': - resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==} + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.19.2': - resolution: {integrity: sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==} + '@floating-ui/react@0.27.16': + resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==} peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} @@ -379,91 +379,30 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@mantine/core@6.0.22': - resolution: {integrity: sha512-6kv0eY7n565fyjgS20qUYeCSxg3f1TJ5vurzbP1HHtFXXKSY0bYoqqDoHipFCt6NxsPQGeiC6cC0c/IWIlxoKQ==} - peerDependencies: - '@mantine/hooks': 6.0.22 - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@mantine/hooks@6.0.22': - resolution: {integrity: sha512-e10//QTN2sAmC4Ryeu5X5L/TsxnrjXMOaGq3dxFPIPsCSwLzyxqySfjzVViWmoPWAj0Ak9MvE2MHFjzmOpA80w==} - peerDependencies: - react: '>=16.8.0' - - '@mantine/notifications@6.0.22': - resolution: {integrity: sha512-x7iIil2yC81fEv/7YK6NYn6CKaftlw0E/hdprmxGWFhy87W9sYiYzPqigXZh11IJZFFW9ZPftpjPQFvDwE4KOw==} - peerDependencies: - '@mantine/core': 6.0.22 - '@mantine/hooks': 6.0.22 - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@mantine/styles@6.0.22': - resolution: {integrity: sha512-Rud/IQp2EFYDiP4csRy2XBrho/Ct+W2/b+XbvCRTeQTmpFy/NfAKm/TWJa5zPvuv/iLTjGkVos9SHw/DteESpQ==} - peerDependencies: - '@emotion/react': '>=11.9.0' - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@mantine/utils@6.0.22': - resolution: {integrity: sha512-RSKlNZvxhMCkOFZ6slbYvZYbWjHUM+PxDQnupIOxIdsTZQQjx/BFfrfJ7kQFOP+g7MtpOds8weAetEs5obwMOQ==} - peerDependencies: - react: '>=16.8.0' - - '@radix-ui/number@1.0.0': - resolution: {integrity: sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==} - - '@radix-ui/primitive@1.0.0': - resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} - - '@radix-ui/react-compose-refs@1.0.0': - resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - - '@radix-ui/react-context@1.0.0': - resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - - '@radix-ui/react-direction@1.0.0': - resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - - '@radix-ui/react-presence@1.0.0': - resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - - '@radix-ui/react-primitive@1.0.1': - resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - - '@radix-ui/react-scroll-area@1.0.2': - resolution: {integrity: sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==} + '@mantine/core@8.3.12': + resolution: {integrity: sha512-bDEoUl4SneltfI1GeEaBk6BVDbLuB/w15YwseAmUvc8ldAbNcsVhxKxY/BdhwqUo6O3L2vhdlb3WwxR1y8741g==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + '@mantine/hooks': 8.3.12 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x - '@radix-ui/react-slot@1.0.1': - resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} + '@mantine/hooks@8.3.12': + resolution: {integrity: sha512-lMMDzDewd3lUNtJCAHDj3g8On9X5aBl4q6EBwgOixKQSby9RG9ASEpK8oYHundHTm9tzo3MDeXWV/z32oSQWuw==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: ^18.x || ^19.x - '@radix-ui/react-use-callback-ref@1.0.0': - resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} + '@mantine/notifications@8.3.12': + resolution: {integrity: sha512-lqPPa11XdcndG8ywMao8yVkMA4jg/pBNbS85bR7OwHQa1yUftmfVlqJl9PZZCiWLX2AgKY3+xM5dHo4LidL+DA==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + '@mantine/core': 8.3.12 + '@mantine/hooks': 8.3.12 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x - '@radix-ui/react-use-layout-effect@1.0.0': - resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} + '@mantine/store@8.3.12': + resolution: {integrity: sha512-EC4eIKpm5s7neMbBrWsP6jGKLqrzHf63Ao3penYr7fn25dFXdbXZYw+IG8GYzxOC4yG61b2zTS+bpy5+vwzXpw==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: ^18.x || ^19.x '@reactflow/background@11.3.14': resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==} @@ -768,10 +707,6 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -795,8 +730,8 @@ packages: classcat@5.0.5: resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} - clsx@1.1.1: - resolution: {integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} convert-source-map@1.9.0: @@ -809,9 +744,6 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} - csstype@3.0.9: - resolution: {integrity: sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1007,6 +939,12 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-number-format@5.4.4: + resolution: {integrity: sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==} + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -1041,14 +979,14 @@ packages: '@types/react': optional: true - react-textarea-autosize@8.3.4: - resolution: {integrity: sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ==} + react-textarea-autosize@8.5.9: + resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-transition-group@4.4.2: - resolution: {integrity: sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==} + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: react: '>=16.6.0' react-dom: '>=16.6.0' @@ -1105,6 +1043,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1508,16 +1450,16 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/dom': 1.7.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/react@0.19.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react@0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - aria-hidden: 1.2.6 + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.10 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tabbable: 6.4.0 @@ -1543,115 +1485,37 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mantine/core@6.0.22(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(@mantine/hooks@6.0.22(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mantine/core@8.3.12(@mantine/hooks@8.3.12(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react': 0.19.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/hooks': 6.0.22(react@18.3.1) - '@mantine/styles': 6.0.22(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/utils': 6.0.22(react@18.3.1) - '@radix-ui/react-scroll-area': 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/react': 0.27.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 8.3.12(react@18.3.1) + clsx: 2.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-number-format: 5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-remove-scroll: 2.7.2(@types/react@18.3.27)(react@18.3.1) - react-textarea-autosize: 8.3.4(@types/react@18.3.27)(react@18.3.1) + react-textarea-autosize: 8.5.9(@types/react@18.3.27)(react@18.3.1) + type-fest: 4.41.0 transitivePeerDependencies: - - '@emotion/react' - '@types/react' - '@mantine/hooks@6.0.22(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@mantine/notifications@6.0.22(@mantine/core@6.0.22(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(@mantine/hooks@6.0.22(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@6.0.22(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mantine/hooks@8.3.12(react@18.3.1)': dependencies: - '@mantine/core': 6.0.22(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(@mantine/hooks@6.0.22(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/hooks': 6.0.22(react@18.3.1) - '@mantine/utils': 6.0.22(react@18.3.1) react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-transition-group: 4.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mantine/styles@6.0.22(@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mantine/notifications@8.3.12(@mantine/core@8.3.12(@mantine/hooks@8.3.12(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.12(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@emotion/react': 11.14.0(@types/react@18.3.27)(react@18.3.1) - clsx: 1.1.1 - csstype: 3.0.9 + '@mantine/core': 8.3.12(@mantine/hooks@8.3.12(react@18.3.1))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 8.3.12(react@18.3.1) + '@mantine/store': 8.3.12(react@18.3.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))(react@18.3.1) - '@mantine/utils@6.0.22(react@18.3.1)': + '@mantine/store@8.3.12(react@18.3.1)': dependencies: react: 18.3.1 - '@radix-ui/number@1.0.0': - dependencies: - '@babel/runtime': 7.28.6 - - '@radix-ui/primitive@1.0.0': - dependencies: - '@babel/runtime': 7.28.6 - - '@radix-ui/react-compose-refs@1.0.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - react: 18.3.1 - - '@radix-ui/react-context@1.0.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - react: 18.3.1 - - '@radix-ui/react-direction@1.0.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - react: 18.3.1 - - '@radix-ui/react-presence@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - '@radix-ui/react-primitive@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - '@radix-ui/react-slot': 1.0.1(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - '@radix-ui/react-scroll-area@1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - '@radix-ui/number': 1.0.0 - '@radix-ui/primitive': 1.0.0 - '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) - '@radix-ui/react-context': 1.0.0(react@18.3.1) - '@radix-ui/react-direction': 1.0.0(react@18.3.1) - '@radix-ui/react-presence': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - '@radix-ui/react-slot@1.0.1(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) - react: 18.3.1 - - '@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - react: 18.3.1 - - '@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.6 - react: 18.3.1 - '@reactflow/background@11.3.14(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@reactflow/core': 11.11.4(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1981,10 +1845,6 @@ snapshots: transitivePeerDependencies: - supports-color - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.28.6 @@ -2007,7 +1867,7 @@ snapshots: classcat@5.0.5: {} - clsx@1.1.1: {} + clsx@2.1.1: {} convert-source-map@1.9.0: {} @@ -2021,8 +1881,6 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - csstype@3.0.9: {} - csstype@3.2.3: {} d3-color@3.1.0: {} @@ -2212,6 +2070,11 @@ snapshots: react-is@16.13.1: {} + react-number-format@5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.27)(react@18.3.1): @@ -2241,7 +2104,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 - react-textarea-autosize@8.3.4(@types/react@18.3.27)(react@18.3.1): + react-textarea-autosize@8.5.9(@types/react@18.3.27)(react@18.3.1): dependencies: '@babel/runtime': 7.28.6 react: 18.3.1 @@ -2250,7 +2113,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-transition-group@4.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.28.6 dom-helpers: 5.2.1 @@ -2334,6 +2197,8 @@ snapshots: tslib@2.8.1: {} + type-fest@4.41.0: {} + typescript@5.9.3: {} update-browserslist-db@1.2.3(browserslist@4.28.1): diff --git a/src/Terrabuild.UI/src/App.tsx b/src/Terrabuild.UI/src/App.tsx index 48a91ac4..c9e2e8f0 100644 --- a/src/Terrabuild.UI/src/App.tsx +++ b/src/Terrabuild.UI/src/App.tsx @@ -1,120 +1,50 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { - Accordion, AppShell, - Badge, Box, - Button, - Checkbox, - Group, - MultiSelect, - Navbar, - NumberInput, - Paper, - ActionIcon, - Select, Stack, - Text, - TextInput, - Title, - useMantineTheme, + rgba, useMantineColorScheme, + useMantineTheme, } from "@mantine/core"; +import { useMediaQuery } from "@mantine/hooks"; import { notifications } from "@mantine/notifications"; -import ReactFlow, { - Background, - Controls, - Node, +import { Edge, + MarkerType, + Node, + OnNodesChange, Position, + ReactFlowInstance, applyNodeChanges, useEdgesState, useNodesState, - ReactFlowInstance, - MarkerType, } from "reactflow"; import "reactflow/dist/style.css"; import dagre from "dagre"; import { Terminal } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import "xterm/css/xterm.css"; +import BuildControlsPanel from "./components/BuildControlsPanel"; +import BuildDetailsPanel from "./components/BuildDetailsPanel"; +import BuildLogPanel from "./components/BuildLogPanel"; +import GraphPanel from "./components/GraphPanel"; +import NodeDetailsPanel from "./components/NodeDetailsPanel"; +import SidebarHeader from "./components/SidebarHeader"; import { - IconAffiliate, - IconCopy, - IconMoon, - IconSun, - IconSquareRoundedChevronDown, -} from "@tabler/icons-react"; + GraphNode, + GraphResponse, + ProjectInfo, + ProjectNode, + ProjectStatus, + TargetSummary, +} from "./types"; -type ProjectInfo = { - id: string; - name?: string | null; - directory: string; - hash: string; -}; - -type GraphNode = { - id: string; - projectId: string; - projectName?: string | null; - projectDir: string; - target: string; - dependencies: string[]; - projectHash: string; - targetHash: string; -}; - -type GraphResponse = { - nodes: Record; - rootNodes?: string[]; - engine?: string | null; - configuration?: string | null; - environment?: string | null; -}; - -type ProjectStatus = { - projectId: string; - status: "success" | "failed"; -}; - -type ProjectNode = { - id: string; - name?: string | null; - directory: string; - hash: string; - targets: GraphNode[]; -}; - -type OperationSummary = { - metaCommand: string; - command: string; - arguments: string; - log: string; - exitCode: number; -}; - -type TargetSummary = { - project: string; - target: string; - operations: OperationSummary[][]; - isSuccessful: boolean; - startedAt: string; - endedAt: string; - duration: string; - cache: string; - outputs?: string | null; -}; +type ProjectStatusMap = Record; const nodeWidth = 320; const nodeHeight = 120; -const engineOptions = [ - { value: "default", label: "Default" }, - { value: "none", label: "None" }, - { value: "docker", label: "Docker" }, - { value: "podman", label: "Podman" }, -]; - const layoutGraph = (nodes: Node[], edges: Edge[]) => { const graph = new dagre.graphlib.Graph(); graph.setDefaultEdgeLabel(() => ({})); @@ -163,9 +93,7 @@ const App = () => { const [selectedTargetKey, setSelectedTargetKey] = useState( null ); - const [projectStatus, setProjectStatus] = useState< - Record - >({}); + const [projectStatus, setProjectStatus] = useState({}); const [showTerminal, setShowTerminal] = useState(false); const [nodeResults, setNodeResults] = useState>( {} @@ -177,16 +105,60 @@ const App = () => { const [nodes, setNodes] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const flowInstance = useRef(null); - const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const { colorScheme } = useMantineColorScheme(); const theme = useMantineTheme(); + const prefersDark = useMediaQuery("(prefers-color-scheme: dark)"); + const effectiveColorScheme = + colorScheme === "auto" ? (prefersDark ? "dark" : "light") : colorScheme; const terminalRef = useRef(null); const terminal = useRef(null); const fitAddon = useRef(null); const logAbort = useRef(null); const terminalReady = useRef(false); + const pendingTargetRef = useRef<{ key: string; target: GraphNode } | null>( + null + ); + const pendingBuildLogRef = useRef(false); + const applyTerminalTheme = () => { + if (!terminal.current || !terminalReady.current) { + return; + } + if (!terminalRef.current || terminalRef.current.offsetWidth === 0) { + return; + } + const darkBackground = theme.colors.dark[7]; + const lightBackground = theme.white; + terminal.current.options.theme = { + background: + effectiveColorScheme === "dark" ? darkBackground : lightBackground, + foreground: effectiveColorScheme === "dark" ? "#d8dbe0" : "#1f2328", + selectionBackground: + effectiveColorScheme === "dark" ? "#3b3f45" : "#c9d0d8", + }; + }; + const flushPendingTerminalActions = () => { + if (!terminalReady.current) { + return; + } + if (pendingTargetRef.current) { + const pending = pendingTargetRef.current; + pendingTargetRef.current = null; + void loadTargetLog(pending.key, pending.target); + } + if (pendingBuildLogRef.current) { + pendingBuildLogRef.current = false; + void startLogStreamInternal(); + } + }; useEffect(() => { + if (!showTerminal) { + return; + } + if (!terminalRef.current || terminal.current) { + return; + } const term = new Terminal({ convertEol: true, scrollback: 3000, @@ -196,65 +168,65 @@ const App = () => { term.loadAddon(fit); terminal.current = term; fitAddon.current = fit; - if (terminalRef.current) { - term.open(terminalRef.current); - term.write("\u001b[?25l"); - terminalReady.current = true; - const resizeObserver = new ResizeObserver(() => { - if (!terminalRef.current) { - return; - } - if (terminalRef.current.offsetWidth === 0) { - return; - } - fit.fit(); - }); - resizeObserver.observe(terminalRef.current); - requestAnimationFrame(() => { - if (!terminalRef.current) { - return; - } - if (terminalRef.current.offsetWidth === 0) { - return; - } - fit.fit(); - }); - return () => { - resizeObserver.disconnect(); - term.write("\u001b[?25h"); - term.dispose(); - }; - } - const handleResize = () => fit.fit(); + term.open(terminalRef.current); + term.write("\u001b[?25l"); + terminalReady.current = true; + applyTerminalTheme(); + flushPendingTerminalActions(); + const resizeObserver = new ResizeObserver(() => { + if (!terminalRef.current) { + return; + } + if ( + terminalRef.current.offsetWidth === 0 || + terminalRef.current.offsetHeight === 0 + ) { + return; + } + fit.fit(); + }); + resizeObserver.observe(terminalRef.current); + requestAnimationFrame(() => { + if (!terminalRef.current) { + return; + } + if ( + terminalRef.current.offsetWidth === 0 || + terminalRef.current.offsetHeight === 0 + ) { + return; + } + fit.fit(); + applyTerminalTheme(); + }); + const handleResize = () => { + if ( + !terminalRef.current || + terminalRef.current.offsetWidth === 0 || + terminalRef.current.offsetHeight === 0 + ) { + return; + } + fit.fit(); + }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); + resizeObserver.disconnect(); + terminalReady.current = false; term.write("\u001b[?25h"); term.dispose(); + terminal.current = null; + fitAddon.current = null; }; - }, []); + }, [showTerminal]); useEffect(() => { - if (!terminal.current) { - return; - } - if (!terminalReady.current) { - return; - } - if (!terminalRef.current || terminalRef.current.offsetWidth === 0) { - return; - } - const darkBackground = theme.colors.dark[7]; - const lightBackground = theme.white; - terminal.current.options.theme = { - background: colorScheme === "dark" ? darkBackground : lightBackground, - foreground: colorScheme === "dark" ? "#d8dbe0" : "#1f2328", - selectionBackground: colorScheme === "dark" ? "#3b3f45" : "#c9d0d8", - }; - }, [colorScheme, theme]); + applyTerminalTheme(); + }, [effectiveColorScheme, theme]); const getNodeStyle = (nodeId: string) => { - const isDark = colorScheme === "dark"; + const isDark = effectiveColorScheme === "dark"; const defaultBorder = isDark ? theme.colors.dark[3] : theme.colors.gray[6]; const selectedBorder = theme.colors.blue[6]; const nodeBackground = isDark ? theme.colors.dark[6] : theme.white; @@ -262,9 +234,9 @@ const App = () => { const status = projectStatus[nodeId]; const statusColor = status === "failed" - ? theme.fn.rgba(theme.colors.red[6], isDark ? 0.35 : 0.15) + ? rgba(theme.colors.red[6], isDark ? 0.35 : 0.15) : status === "success" - ? theme.fn.rgba(theme.colors.green[6], isDark ? 0.35 : 0.15) + ? rgba(theme.colors.green[6], isDark ? 0.35 : 0.15) : null; return { borderRadius: 12, @@ -301,7 +273,7 @@ const App = () => { style: getNodeStyle(node.id), })) ); - }, [selectedNodeId, colorScheme, theme, projectStatus, setNodes]); + }, [selectedNodeId, effectiveColorScheme, theme, projectStatus, setNodes]); useEffect(() => { const load = async () => { @@ -360,7 +332,7 @@ const App = () => { ); if (statusResponse.ok) { const statusData = (await statusResponse.json()) as ProjectStatus[]; - const statusMap: Record = {}; + const statusMap: ProjectStatusMap = {}; statusData.forEach((item) => { statusMap[item.projectId] = item.status; }); @@ -397,7 +369,7 @@ const App = () => { } }); - const isDark = colorScheme === "dark"; + const isDark = effectiveColorScheme === "dark"; const edgeStroke = isDark ? theme.colors.dark[3] : theme.colors.gray[5]; const flowNodes: Node[] = Array.from(projectMap.values()) @@ -453,7 +425,7 @@ const App = () => { }); }); return layoutGraph(flowNodes, flowEdges); - }, [graph, selectedNodeId, layoutVersion, colorScheme, theme]); + }, [graph, selectedNodeId, layoutVersion, effectiveColorScheme, theme]); const nodeCount = graph ? Object.keys(graph.nodes).length : 0; const rootNodeCount = graph?.rootNodes?.length ?? 0; @@ -499,21 +471,7 @@ const App = () => { }); }, [graph, baseGraph, manualPositions, setNodes, setEdges]); - const handleSelectTargets = (event: React.ChangeEvent) => { - const values = Array.from(event.target.selectedOptions).map( - (option) => option.value - ); - setSelectedTargets(values); - }; - - const handleSelectProjects = (event: React.ChangeEvent) => { - const values = Array.from(event.target.selectedOptions).map( - (option) => option.value - ); - setSelectedProjects(values); - }; - - const startLogStream = async () => { + const startLogStreamInternal = async () => { if (!terminal.current) { return; } @@ -576,6 +534,15 @@ const App = () => { } setBuildRunning(false); }; + + const startLogStream = async () => { + if (!terminal.current) { + pendingBuildLogRef.current = true; + setShowTerminal(true); + return; + } + await startLogStreamInternal(); + }; const buildPayload = () => { const parallel = @@ -705,7 +672,7 @@ const App = () => { ); if (statusResponse.ok) { const statusData = (await statusResponse.json()) as ProjectStatus[]; - const statusMap: Record = {}; + const statusMap: ProjectStatusMap = {}; statusData.forEach((item) => { statusMap[item.projectId] = item.status; }); @@ -713,7 +680,14 @@ const App = () => { } }; refresh().catch(() => null); - }, [buildRunning, selectedTargets, selectedProjects, configuration, environment, engine]); + }, [ + buildRunning, + selectedTargets, + selectedProjects, + configuration, + environment, + engine, + ]); const loadProjectResults = async (project: ProjectNode) => { setSelectedProject(project); @@ -742,19 +716,7 @@ const App = () => { } }; - const buildTargetLog = (summary: TargetSummary) => { - return summary.operations - .flatMap((group) => - group.map((operation) => { - const header = operation.metaCommand || operation.command; - return `${header}\n${operation.log || ""}`.trim(); - }) - ) - .filter((value) => value.length > 0) - .join("\n\n"); - }; - - const showTargetLog = async (key: string, target: GraphNode) => { + const loadTargetLog = async (key: string, target: GraphNode) => { if (!terminal.current) { return; } @@ -779,492 +741,157 @@ const App = () => { } }; + const showTargetLog = async (key: string, target: GraphNode) => { + if (!terminal.current) { + pendingTargetRef.current = { key, target }; + setShowTerminal(true); + return; + } + await loadTargetLog(key, target); + }; + + const handleNodesChange: OnNodesChange = (changes) => { + setNodes((current) => { + const updated = applyNodeChanges(changes, current); + const positions: Record = {}; + updated.forEach((node) => { + positions[node.id] = node.position; + if (node.selected) { + setSelectedNodeId(node.id); + } + }); + setManualPositions(positions); + return updated; + }); + }; + + const terminalBackground = + effectiveColorScheme === "dark" ? theme.colors.dark[7] : theme.white; + return ( + + - - - - - - - Terrabuild - - Graph Console - - - toggleColorScheme()} - variant="light" - size="lg" - aria-label="Toggle color scheme" - > - {colorScheme === "dark" ? ( - - ) : ( - - )} - - - - - - - - - - ({ value: target, label: target }))} - label="Targets" - placeholder="Select targets" - searchable - nothingFound="No targets" - value={selectedTargets} - onChange={(values) => setSelectedTargets(values)} - /> - - - { - const checked = event.currentTarget.checked; - setForceBuild(checked); - if (checked) { - setRetryBuild(false); - } - }} - /> - { - const checked = event.currentTarget.checked; - setRetryBuild(checked); - if (checked) { - setForceBuild(false); - } - }} - /> - - - - - Advanced - - - project.name) - .map((project) => ({ - value: project.name as string, - label: project.name as string, - }))} - label="Projects" - placeholder="Select projects" - searchable - nothingFound="No projects" - value={selectedProjects} - onChange={(values) => setSelectedProjects(values)} - /> - - - setConfiguration(event.currentTarget.value) - } - /> - - - setEnvironment(event.currentTarget.value) - } - /> - - onEngineChange(value ?? "default")} + /> + + { + if (value === "" || value === null) { + onParallelismChange(""); + } else { + onParallelismChange(String(value)); + } + }} + /> + + + + + + + + + + + + + {buildError && ( + + {buildError} + + )} + + + ); +}; + +export default BuildControlsPanel; diff --git a/src/Terrabuild.UI/src/components/BuildDetailsPanel.tsx b/src/Terrabuild.UI/src/components/BuildDetailsPanel.tsx new file mode 100644 index 00000000..4effd7ad --- /dev/null +++ b/src/Terrabuild.UI/src/components/BuildDetailsPanel.tsx @@ -0,0 +1,78 @@ +import { Group, Paper, Stack, Text } from "@mantine/core"; +import { GraphResponse } from "../types"; + +type BuildDetailsPanelProps = { + graph: GraphResponse | null; + nodeCount: number; + rootNodeCount: number; + configurationLabel: string; + environmentLabel: string; + engineLabel: string; +}; + +const BuildDetailsPanel = ({ + graph, + nodeCount, + rootNodeCount, + configurationLabel, + environmentLabel, + engineLabel, +}: BuildDetailsPanelProps) => { + return ( + + + Build Details + {graph ? ( + <> + + + Nodes + + + {nodeCount} + + + + + Root nodes + + + {rootNodeCount} + + + + + Configuration + + + {configurationLabel} + + + + + Environment + + + {environmentLabel} + + + + + Engine + + + {engineLabel} + + + + ) : ( + + Select targets to load build details. + + )} + + + ); +}; + +export default BuildDetailsPanel; diff --git a/src/Terrabuild.UI/src/components/BuildLogPanel.tsx b/src/Terrabuild.UI/src/components/BuildLogPanel.tsx new file mode 100644 index 00000000..8accf8c8 --- /dev/null +++ b/src/Terrabuild.UI/src/components/BuildLogPanel.tsx @@ -0,0 +1,63 @@ +import { ActionIcon, Badge, Box, Group, Paper, Title } from "@mantine/core"; +import { IconSquareRoundedChevronDown } from "@tabler/icons-react"; +import { RefObject } from "react"; + +type BuildLogPanelProps = { + showTerminal: boolean; + buildRunning: boolean; + onHide: () => void; + terminalRef: RefObject; + background: string; +}; + +const BuildLogPanel = ({ + showTerminal, + buildRunning, + onHide, + terminalRef, + background, +}: BuildLogPanelProps) => { + return ( + + {showTerminal && ( + + Build Log + + + {buildRunning ? "Live" : "Idle"} + + + + + + + )} + + + ); +}; + +export default BuildLogPanel; diff --git a/src/Terrabuild.UI/src/components/GraphPanel.tsx b/src/Terrabuild.UI/src/components/GraphPanel.tsx new file mode 100644 index 00000000..19a28fa9 --- /dev/null +++ b/src/Terrabuild.UI/src/components/GraphPanel.tsx @@ -0,0 +1,109 @@ +import { ActionIcon, Box, Group, Paper, Text, Title } from "@mantine/core"; +import { IconAffiliate } from "@tabler/icons-react"; +import ReactFlow, { + Background, + Controls, + Edge, + Node, + NodeMouseHandler, + OnEdgesChange, + OnNodesChange, + ReactFlowInstance, +} from "reactflow"; +import { GraphResponse } from "../types"; + +type GraphPanelProps = { + graph: GraphResponse | null; + graphError: string | null; + nodes: Node[]; + edges: Edge[]; + onInit: (instance: ReactFlowInstance) => void; + onNodesChange: OnNodesChange; + onEdgesChange: OnEdgesChange; + onNodeClick: NodeMouseHandler; + onReflow: () => void; +}; + +const GraphPanel = ({ + graph, + graphError, + nodes, + edges, + onInit, + onNodesChange, + onEdgesChange, + onNodeClick, + onReflow, +}: GraphPanelProps) => { + return ( + + + + Execution Graph + {graphError && ( + + {graphError} + + )} + + + + + + + + + {graph ? ( + + + + + + + ) : ( + + + Select at least one target to view the graph. + + + )} + + + ); +}; + +export default GraphPanel; diff --git a/src/Terrabuild.UI/src/components/NodeDetailsPanel.tsx b/src/Terrabuild.UI/src/components/NodeDetailsPanel.tsx new file mode 100644 index 00000000..20816ccb --- /dev/null +++ b/src/Terrabuild.UI/src/components/NodeDetailsPanel.tsx @@ -0,0 +1,61 @@ +import { Badge, Button, Paper, Stack, Text } from "@mantine/core"; +import { GraphNode, ProjectNode, TargetSummary } from "../types"; + +type NodeDetailsPanelProps = { + selectedProject: ProjectNode | null; + selectedTargetKey: string | null; + nodeResults: Record; + onSelectTarget: (key: string, target: GraphNode) => void; +}; + +const NodeDetailsPanel = ({ + selectedProject, + selectedTargetKey, + nodeResults, + onSelectTarget, +}: NodeDetailsPanelProps) => { + return ( + + + Node Details + {selectedProject ? ( + <> + {selectedProject.directory} + + {selectedProject.targets.map((target) => { + const cacheKey = + `${target.projectHash}/${target.target}/${target.targetHash}`; + const summary = nodeResults[cacheKey]; + return ( + + ); + })} + + + ) : ( + + Select a node in the graph to inspect it. + + )} + + + ); +}; + +export default NodeDetailsPanel; diff --git a/src/Terrabuild.UI/src/components/SidebarHeader.tsx b/src/Terrabuild.UI/src/components/SidebarHeader.tsx new file mode 100644 index 00000000..9243613d --- /dev/null +++ b/src/Terrabuild.UI/src/components/SidebarHeader.tsx @@ -0,0 +1,63 @@ +import { Box, Group, Text, Title } from "@mantine/core"; +import ThemeSwitcher from "./ThemeSwitcher"; + +const SidebarHeader = () => { + return ( + + + + + + Terrabuild + + Graph Console + + + + + ); +}; + +export default SidebarHeader; diff --git a/src/Terrabuild.UI/src/components/ThemeSwitcher.tsx b/src/Terrabuild.UI/src/components/ThemeSwitcher.tsx new file mode 100644 index 00000000..0dae37c3 --- /dev/null +++ b/src/Terrabuild.UI/src/components/ThemeSwitcher.tsx @@ -0,0 +1,39 @@ +import { ActionIcon, useMantineColorScheme } from "@mantine/core"; +import { IconMoon, IconSun, IconSunMoon } from "@tabler/icons-react"; + +const ThemeSwitcher = () => { + const { colorScheme, setColorScheme } = useMantineColorScheme(); + + const ThemeIcon = () => { + if (colorScheme === "auto") { + return ; + } + if (colorScheme === "light") { + return ; + } + return ; + }; + + const toggleTheme = () => { + if (colorScheme === "auto") { + setColorScheme("light"); + } else if (colorScheme === "light") { + setColorScheme("dark"); + } else { + setColorScheme("auto"); + } + }; + + return ( + + + + ); +}; + +export default ThemeSwitcher; diff --git a/src/Terrabuild.UI/src/main.tsx b/src/Terrabuild.UI/src/main.tsx index 6f7181d9..24a5268e 100644 --- a/src/Terrabuild.UI/src/main.tsx +++ b/src/Terrabuild.UI/src/main.tsx @@ -1,49 +1,43 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core"; +import { + MantineProvider, + createTheme, + localStorageColorSchemeManager, +} from "@mantine/core"; import { Notifications } from "@mantine/notifications"; -import { useLocalStorage } from "@mantine/hooks"; +import "@mantine/core/styles.css"; +import "@mantine/notifications/styles.css"; import App from "./App"; import "./styles.css"; -const Root = () => { - const [colorScheme, setColorScheme] = useLocalStorage({ - key: "tb-color-scheme", - defaultValue: "dark", - getInitialValueInEffect: true, - }); - - const toggleColorScheme = (value?: ColorScheme) => - setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")); +const colorSchemeManager = localStorageColorSchemeManager({ + key: "tb-color-scheme", +}); - const notificationTheme = { - colorScheme, - components: { - Notification: { - styles: (theme: any) => ({ - root: { - border: `1px solid ${theme.colors.gray[3]}`, - borderRadius: 8, - }, - }), - }, +const theme = createTheme({ + components: { + Notification: { + styles: (mantineTheme) => ({ + root: { + border: `1px solid ${mantineTheme.colors.gray[3]}`, + borderRadius: 8, + }, + }), }, - }; + }, +}); +const Root = () => { return ( - - - - - - + + + ); }; diff --git a/src/Terrabuild.UI/src/styles.css b/src/Terrabuild.UI/src/styles.css index 6401d5b4..d125baa7 100644 --- a/src/Terrabuild.UI/src/styles.css +++ b/src/Terrabuild.UI/src/styles.css @@ -15,3 +15,23 @@ transform: translateY(0); } } +.reactflow-wrapper { + width: 100%; + height: 100%; +} + +.reactflow-wrapper .react-flow { + width: 100%; + height: 100%; +} + +.reactflow-wrapper .react-flow__renderer { + width: 100%; + height: 100%; +} + +html, +body, +#root { + height: 100%; +} diff --git a/src/Terrabuild.UI/src/types.ts b/src/Terrabuild.UI/src/types.ts new file mode 100644 index 00000000..0412c895 --- /dev/null +++ b/src/Terrabuild.UI/src/types.ts @@ -0,0 +1,58 @@ +export type ProjectInfo = { + id: string; + name?: string | null; + directory: string; + hash: string; +}; + +export type GraphNode = { + id: string; + projectId: string; + projectName?: string | null; + projectDir: string; + target: string; + dependencies: string[]; + projectHash: string; + targetHash: string; +}; + +export type GraphResponse = { + nodes: Record; + rootNodes?: string[]; + engine?: string | null; + configuration?: string | null; + environment?: string | null; +}; + +export type ProjectStatus = { + projectId: string; + status: "success" | "failed"; +}; + +export type ProjectNode = { + id: string; + name?: string | null; + directory: string; + hash: string; + targets: GraphNode[]; +}; + +export type OperationSummary = { + metaCommand: string; + command: string; + arguments: string; + log: string; + exitCode: number; +}; + +export type TargetSummary = { + project: string; + target: string; + operations: OperationSummary[][]; + isSuccessful: boolean; + startedAt: string; + endedAt: string; + duration: string; + cache: string; + outputs?: string | null; +};