diff --git a/package.json b/package.json index 57847fd754..0a45777ad4 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "@bugsnag/js": "^8.10.0", "@bugsnag/plugin-react": "^8.9.0", "@dagrejs/dagre": "^3.0.0", - "@dnd-kit/core": "^6.3.1", "@hey-api/client-fetch": "^0.13.1", "@hyperjump/browser": "^1.4.0", "@hyperjump/json-schema": "^1.17.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94ee98d96e..473c830d82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@dagrejs/dagre': specifier: ^3.0.0 version: 3.0.0 - '@dnd-kit/core': - specifier: ^6.3.1 - version: 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@hey-api/client-fetch': specifier: ^0.13.1 version: 0.13.1(@hey-api/openapi-ts@0.99.0(magicast@0.5.4)(typescript@5.9.3)) @@ -346,15 +343,15 @@ packages: resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} '@apm-js-collab/code-transformer-bundler-plugins@0.7.3': - resolution: {integrity: sha512-qNbPwuMZ8f5ZuGj/ttPeB7a6C/S1bB6tNYaEL5vNiRKydSAxa4AU0gxCWgaP4fVju+AuwhcumSFjrEcGF9Dv7Q==} + resolution: {integrity: sha512-qNbPwuMZ8f5ZuGj/ttPeB7a6C/S1bB6tNYaEL5vNiRKydSAxa4AU0gxCWgaP4fVju+AuwhcumSFjrEcGF9Dv7Q==, tarball: https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.7.3.tgz} engines: {node: '>=18.0.0'} '@apm-js-collab/code-transformer@0.18.1': - resolution: {integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==} + resolution: {integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==, tarball: https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.18.1.tgz} hasBin: true '@apm-js-collab/tracing-hooks@0.13.0': - resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==} + resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==, tarball: https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.13.0.tgz} '@asamuzakjp/css-color@5.1.11': resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} @@ -372,7 +369,7 @@ packages: resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} '@astrojs/compiler@4.0.0': - resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==} + resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==, tarball: https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -522,7 +519,7 @@ packages: hasBin: true '@babel/parser@7.29.8': - resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==, tarball: https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz} engines: {node: '>=6.0.0'} hasBin: true @@ -586,7 +583,7 @@ packages: engines: {node: '>=6.9.0'} '@babel/types@7.29.8': - resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==, tarball: https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@1.0.2': @@ -672,189 +669,173 @@ packages: '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} - '@dnd-kit/accessibility@3.1.1': - resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} - peerDependencies: - react: '>=16.8.0' - - '@dnd-kit/core@6.3.1': - resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@dnd-kit/utilities@3.2.2': - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} - peerDependencies: - react: '>=16.8.0' - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==, tarball: https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz} '@emnapi/core@1.11.2': - resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==, tarball: https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz} '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==, tarball: https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz} '@emnapi/runtime@1.11.2': - resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==, tarball: https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz} '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==, tarball: https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz} '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [aix] '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [android] '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm] os: [android] '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [android] '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [darwin] '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [freebsd] '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz} engines: {node: '>=18'} cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz} engines: {node: '>=18'} cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz} engines: {node: '>=18'} cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz} engines: {node: '>=18'} cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz} engines: {node: '>=18'} cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz} engines: {node: '>=18'} cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [linux] '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [netbsd] '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [openbsd] '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [sunos] '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz} engines: {node: '>=18'} cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz} engines: {node: '>=18'} cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1002,7 +983,7 @@ packages: resolution: {integrity: sha512-aUVWwu2GtC/cU4DwdTM+b+5jjboM6wft6vNg1+7pUTk/nNRNaBYcYhaEnCir9eRLBbkcGPkiEO7XRUlcDkxj4Q==} '@iarna/toml@2.2.5': - resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==, tarball: https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1028,7 +1009,7 @@ packages: engines: {node: '>=8'} '@modelcontextprotocol/sdk@1.29.0': - resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==, tarball: https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -1048,7 +1029,7 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==, tarball: https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -1089,276 +1070,292 @@ packages: zod: ^4.0.0 '@opentelemetry/api-logs@0.220.0': - resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==, tarball: https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.1': - resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==, tarball: https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz} engines: {node: '>=8.0.0'} '@opentelemetry/core@2.10.0': - resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} + resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==, tarball: https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' '@opentelemetry/instrumentation@0.220.0': - resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==, tarball: https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 '@opentelemetry/resources@2.10.0': - resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} + resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==, tarball: https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' '@opentelemetry/sdk-trace-base@2.10.0': - resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} + resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==, tarball: https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' '@opentelemetry/sdk-trace@2.10.0': - resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==} + resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==, tarball: https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' '@opentelemetry/semantic-conventions@1.43.0': - resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==, tarball: https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz} engines: {node: '>=14'} '@oxc-parser/binding-android-arm-eabi@0.140.0': - resolution: {integrity: sha512-ZfjDZ422mo7eo3b3VltqNsV9kmv1qt/sPEAMSl64iOSwhVfd0eIZ9LB79Mbs1xYXJnk7WSROwzBCKDIiVxPTvQ==} + resolution: {integrity: sha512-ZfjDZ422mo7eo3b3VltqNsV9kmv1qt/sPEAMSl64iOSwhVfd0eIZ9LB79Mbs1xYXJnk7WSROwzBCKDIiVxPTvQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] '@oxc-parser/binding-android-arm-eabi@0.143.0': - resolution: {integrity: sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==} + resolution: {integrity: sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==, tarball: https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] '@oxc-parser/binding-android-arm64@0.140.0': - resolution: {integrity: sha512-Ia8jSvikUX6Sf+Ht+KOCUF/k1HpR0VlmqIYymubmWDebOEGtsyliHDR6JxsZ4IX3/c/GbrB1uh09aVGQv/LQmQ==} + resolution: {integrity: sha512-Ia8jSvikUX6Sf+Ht+KOCUF/k1HpR0VlmqIYymubmWDebOEGtsyliHDR6JxsZ4IX3/c/GbrB1uh09aVGQv/LQmQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] '@oxc-parser/binding-android-arm64@0.143.0': - resolution: {integrity: sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==} + resolution: {integrity: sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] '@oxc-parser/binding-darwin-arm64@0.140.0': - resolution: {integrity: sha512-G6VK0nK61pH0d0mBjUqSZbVxGqqO5uzeginLDQj+gOO6ObfJjXRwgkD/ol0w1INcnFeAb6YGGO7qc3ueGHaycQ==} + resolution: {integrity: sha512-G6VK0nK61pH0d0mBjUqSZbVxGqqO5uzeginLDQj+gOO6ObfJjXRwgkD/ol0w1INcnFeAb6YGGO7qc3ueGHaycQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] '@oxc-parser/binding-darwin-arm64@0.143.0': - resolution: {integrity: sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==} + resolution: {integrity: sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==, tarball: https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] '@oxc-parser/binding-darwin-x64@0.140.0': - resolution: {integrity: sha512-HazBOuZzd2pO1C2uMmp8Gv7mhzMHqKSKDS1OZfcLEvpIcgA+48J92HEtNanVHDIzRD9PRPCV6aS6fkZIWOVl8Q==} + resolution: {integrity: sha512-HazBOuZzd2pO1C2uMmp8Gv7mhzMHqKSKDS1OZfcLEvpIcgA+48J92HEtNanVHDIzRD9PRPCV6aS6fkZIWOVl8Q==, tarball: https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] '@oxc-parser/binding-darwin-x64@0.143.0': - resolution: {integrity: sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==} + resolution: {integrity: sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] '@oxc-parser/binding-freebsd-x64@0.140.0': - resolution: {integrity: sha512-9hSUU+HmTUyOe4JzMHxNGgLWNY7rrO+6ShicZwImNJacEAACDMIkuEQQkvXSL+WJN50jaNtLYJv8s4OcBdpyUQ==} + resolution: {integrity: sha512-9hSUU+HmTUyOe4JzMHxNGgLWNY7rrO+6ShicZwImNJacEAACDMIkuEQQkvXSL+WJN50jaNtLYJv8s4OcBdpyUQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] '@oxc-parser/binding-freebsd-x64@0.143.0': - resolution: {integrity: sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==} + resolution: {integrity: sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] '@oxc-parser/binding-linux-arm-gnueabihf@0.140.0': - resolution: {integrity: sha512-RAEuQsYtS0KcDFqN0ABTjyyNlokS91JeuDuoW9tEbG0JTbRNXnpQUdbYc/16JoA6Z/2ALbNrE3KmxtqDiuIjCQ==} + resolution: {integrity: sha512-RAEuQsYtS0KcDFqN0ABTjyyNlokS91JeuDuoW9tEbG0JTbRNXnpQUdbYc/16JoA6Z/2ALbNrE3KmxtqDiuIjCQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxc-parser/binding-linux-arm-gnueabihf@0.143.0': - resolution: {integrity: sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==} + resolution: {integrity: sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxc-parser/binding-linux-arm-musleabihf@0.140.0': - resolution: {integrity: sha512-c4CkHvPvqfojouredJ0w3e6+jiBq0SbFyhH61kr/zPb/7XsaYTNKQ54vmlSsopfdQbNDX40ZeK9Abs2Qet6wcw==} + resolution: {integrity: sha512-c4CkHvPvqfojouredJ0w3e6+jiBq0SbFyhH61kr/zPb/7XsaYTNKQ54vmlSsopfdQbNDX40ZeK9Abs2Qet6wcw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxc-parser/binding-linux-arm-musleabihf@0.143.0': - resolution: {integrity: sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==} + resolution: {integrity: sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxc-parser/binding-linux-arm64-gnu@0.140.0': - resolution: {integrity: sha512-yrjmLj8ixPB25yqvPGr28meGjb+keed7m1GqqY/0uqkhZIoT4t9zmfwUgFEtC33C7dtE+UQ7TU0IaVxf97SWJg==} + resolution: {integrity: sha512-yrjmLj8ixPB25yqvPGr28meGjb+keed7m1GqqY/0uqkhZIoT4t9zmfwUgFEtC33C7dtE+UQ7TU0IaVxf97SWJg==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-arm64-gnu@0.143.0': - resolution: {integrity: sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==} + resolution: {integrity: sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.140.0': - resolution: {integrity: sha512-ggGMQTN8Agwxp2WiLMpdY671dt0qTDJWiWlJeig3HnUwTnerRl0J2JdGVghWBeDcss2D9S2V2Js6dZHEiVabVA==} + resolution: {integrity: sha512-ggGMQTN8Agwxp2WiLMpdY671dt0qTDJWiWlJeig3HnUwTnerRl0J2JdGVghWBeDcss2D9S2V2Js6dZHEiVabVA==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-arm64-musl@0.143.0': - resolution: {integrity: sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==} + resolution: {integrity: sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.140.0': - resolution: {integrity: sha512-IgTs8xYAFgAUGNmR65tIqjlJ8vKgrfXzC515e9goSdfMyKQV4aJpd2pUUudU4u51G64H0/DSEJEXKOraxm9ZCA==} + resolution: {integrity: sha512-IgTs8xYAFgAUGNmR65tIqjlJ8vKgrfXzC515e9goSdfMyKQV4aJpd2pUUudU4u51G64H0/DSEJEXKOraxm9ZCA==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-ppc64-gnu@0.143.0': - resolution: {integrity: sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==} + resolution: {integrity: sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.140.0': - resolution: {integrity: sha512-A1x+PMWZmSGaFVOx2YeNTFau8uD+QO14/vLP4GrcuvUPs3+nBkUOjy9Lus86ftHsDojjYMbvBelmKc3F7Rv08g==} + resolution: {integrity: sha512-A1x+PMWZmSGaFVOx2YeNTFau8uD+QO14/vLP4GrcuvUPs3+nBkUOjy9Lus86ftHsDojjYMbvBelmKc3F7Rv08g==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.143.0': - resolution: {integrity: sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==} + resolution: {integrity: sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.140.0': - resolution: {integrity: sha512-zBqpfRo2myWPrPo5xUjeZqlnPXPXsX8BcWtWff66/eGRQdbPjhzPgXa/F+AtxT2afUViPxbuDlwscMKzQ5tg+g==} + resolution: {integrity: sha512-zBqpfRo2myWPrPo5xUjeZqlnPXPXsX8BcWtWff66/eGRQdbPjhzPgXa/F+AtxT2afUViPxbuDlwscMKzQ5tg+g==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-riscv64-musl@0.143.0': - resolution: {integrity: sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==} + resolution: {integrity: sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.140.0': - resolution: {integrity: sha512-2M1DPm/8w9I//YzFlFC9qXw+r2tJFh5CYwRlYTq2vUJQS7qoQftEDeCZ8EnN7KHtvSiXvYj8mZI5pR7DpXmcEw==} + resolution: {integrity: sha512-2M1DPm/8w9I//YzFlFC9qXw+r2tJFh5CYwRlYTq2vUJQS7qoQftEDeCZ8EnN7KHtvSiXvYj8mZI5pR7DpXmcEw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-s390x-gnu@0.143.0': - resolution: {integrity: sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==} + resolution: {integrity: sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.140.0': - resolution: {integrity: sha512-8aRDbZ/U/jO8N7go1MO72jtbpb4uswV8d7vOkMvt/BPgZiyEYvl1VIWK4ESxZZhnJ4tqwVldgX7dNiP/eB1Jdg==} + resolution: {integrity: sha512-8aRDbZ/U/jO8N7go1MO72jtbpb4uswV8d7vOkMvt/BPgZiyEYvl1VIWK4ESxZZhnJ4tqwVldgX7dNiP/eB1Jdg==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.143.0': - resolution: {integrity: sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==} + resolution: {integrity: sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.140.0': - resolution: {integrity: sha512-xRqpeI8U2sQQS1W5BMWRyMTxtagkuLG2dEWruet5lFsWHTvBth11/TpSaJatHdqVVwHN0q3uuoS9zRsGinq8hg==} + resolution: {integrity: sha512-xRqpeI8U2sQQS1W5BMWRyMTxtagkuLG2dEWruet5lFsWHTvBth11/TpSaJatHdqVVwHN0q3uuoS9zRsGinq8hg==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-x64-musl@0.143.0': - resolution: {integrity: sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==} + resolution: {integrity: sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.140.0': - resolution: {integrity: sha512-GbGRe26MqAKciFRvXeHNQJ6VAHYs9R4miP89sEAncysM3n+f4lnyLWgsa9kklJNpfnxdq2yRoNYHFqwBckVimw==} + resolution: {integrity: sha512-GbGRe26MqAKciFRvXeHNQJ6VAHYs9R4miP89sEAncysM3n+f4lnyLWgsa9kklJNpfnxdq2yRoNYHFqwBckVimw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] '@oxc-parser/binding-openharmony-arm64@0.143.0': - resolution: {integrity: sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==} + resolution: {integrity: sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] '@oxc-parser/binding-wasm32-wasi@0.140.0': - resolution: {integrity: sha512-vFiC1hqys+hkX1GnQkIoiTQJNiUm43Z0lO35ETKXTw0YtpW7+cN58YRRXFAQQ+TgpkIi3lrhcxdlnqz+Oi3ptQ==} + resolution: {integrity: sha512-vFiC1hqys+hkX1GnQkIoiTQJNiUm43Z0lO35ETKXTw0YtpW7+cN58YRRXFAQQ+TgpkIi3lrhcxdlnqz+Oi3ptQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] '@oxc-parser/binding-win32-arm64-msvc@0.140.0': - resolution: {integrity: sha512-fGSQldwEYKhM+H8uLt76Op8hh5+FYaR6lvvQ1Txw3Mhn86DyQXLcI0fi1EkFlTK7F+46OCk/j0AJMzZQm6g5Xg==} + resolution: {integrity: sha512-fGSQldwEYKhM+H8uLt76Op8hh5+FYaR6lvvQ1Txw3Mhn86DyQXLcI0fi1EkFlTK7F+46OCk/j0AJMzZQm6g5Xg==, tarball: https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] '@oxc-parser/binding-win32-arm64-msvc@0.143.0': - resolution: {integrity: sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==} + resolution: {integrity: sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] '@oxc-parser/binding-win32-ia32-msvc@0.140.0': - resolution: {integrity: sha512-sDS2Bai+g3ZWYwfZqmosiSuFDBcVnZ3Ta6pszzsiJoLMqsJEWKcxXXbGa7b7yXr++W2lQNPb3ZRJ8czseqL7RA==} + resolution: {integrity: sha512-sDS2Bai+g3ZWYwfZqmosiSuFDBcVnZ3Ta6pszzsiJoLMqsJEWKcxXXbGa7b7yXr++W2lQNPb3ZRJ8czseqL7RA==, tarball: https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] '@oxc-parser/binding-win32-ia32-msvc@0.143.0': - resolution: {integrity: sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==} + resolution: {integrity: sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==, tarball: https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] '@oxc-parser/binding-win32-x64-msvc@0.140.0': - resolution: {integrity: sha512-kHbE1zWyb5OQgJA6/5P4WjiuB01sYdQwtZnSSyE58FQEXDAMnyeeq4vj7KgN75i5SlBzOs8A5MrtlD3gOlDKqQ==} + resolution: {integrity: sha512-kHbE1zWyb5OQgJA6/5P4WjiuB01sYdQwtZnSSyE58FQEXDAMnyeeq4vj7KgN75i5SlBzOs8A5MrtlD3gOlDKqQ==, tarball: https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.140.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] '@oxc-parser/binding-win32-x64-msvc@0.143.0': - resolution: {integrity: sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==} + resolution: {integrity: sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==, tarball: https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1370,213 +1367,229 @@ packages: resolution: {integrity: sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==} '@oxc-project/types@0.143.0': - resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==, tarball: https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz} '@oxc-resolver/binding-android-arm-eabi@11.24.2': - resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz} cpu: [arm] os: [android] '@oxc-resolver/binding-android-arm64@11.24.2': - resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz} cpu: [arm64] os: [android] '@oxc-resolver/binding-darwin-arm64@11.24.2': - resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz} cpu: [arm64] os: [darwin] '@oxc-resolver/binding-darwin-x64@11.24.2': - resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz} cpu: [x64] os: [darwin] '@oxc-resolver/binding-freebsd-x64@11.24.2': - resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz} cpu: [x64] os: [freebsd] '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': - resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz} cpu: [arm] os: [linux] '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': - resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz} cpu: [arm] os: [linux] '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': - resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.24.2': - resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': - resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': - resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': - resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': - resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.24.2': - resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.24.2': - resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.24.2': - resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz} cpu: [arm64] os: [openharmony] '@oxc-resolver/binding-wasm32-wasi@11.24.2': - resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz} engines: {node: '>=14.0.0'} cpu: [wasm32] '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': - resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz} cpu: [arm64] os: [win32] '@oxc-resolver/binding-win32-x64-msvc@11.24.2': - resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz} cpu: [x64] os: [win32] '@oxlint/binding-android-arm-eabi@1.77.0': - resolution: {integrity: sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==} + resolution: {integrity: sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==, tarball: https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] '@oxlint/binding-android-arm64@1.77.0': - resolution: {integrity: sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==} + resolution: {integrity: sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==, tarball: https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] '@oxlint/binding-darwin-arm64@1.77.0': - resolution: {integrity: sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==} + resolution: {integrity: sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==, tarball: https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] '@oxlint/binding-darwin-x64@1.77.0': - resolution: {integrity: sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==} + resolution: {integrity: sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==, tarball: https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] '@oxlint/binding-freebsd-x64@1.77.0': - resolution: {integrity: sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==} + resolution: {integrity: sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==, tarball: https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] '@oxlint/binding-linux-arm-gnueabihf@1.77.0': - resolution: {integrity: sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==} + resolution: {integrity: sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxlint/binding-linux-arm-musleabihf@1.77.0': - resolution: {integrity: sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==} + resolution: {integrity: sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@oxlint/binding-linux-arm64-gnu@1.77.0': - resolution: {integrity: sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==} + resolution: {integrity: sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.77.0': - resolution: {integrity: sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==} + resolution: {integrity: sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.77.0': - resolution: {integrity: sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==} + resolution: {integrity: sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.77.0': - resolution: {integrity: sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==} + resolution: {integrity: sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.77.0': - resolution: {integrity: sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==} + resolution: {integrity: sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.77.0': - resolution: {integrity: sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==} + resolution: {integrity: sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.77.0': - resolution: {integrity: sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==} + resolution: {integrity: sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.77.0': - resolution: {integrity: sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==} + resolution: {integrity: sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==, tarball: https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.77.0': - resolution: {integrity: sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==} + resolution: {integrity: sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==, tarball: https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] '@oxlint/binding-win32-arm64-msvc@1.77.0': - resolution: {integrity: sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==} + resolution: {integrity: sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==, tarball: https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] '@oxlint/binding-win32-ia32-msvc@1.77.0': - resolution: {integrity: sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==} + resolution: {integrity: sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==, tarball: https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] '@oxlint/binding-win32-x64-msvc@1.77.0': - resolution: {integrity: sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==} + resolution: {integrity: sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==, tarball: https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2116,8 +2129,8 @@ packages: '@radix-ui/rect@1.1.2': resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} - '@react-grab/cli@0.1.50': - resolution: {integrity: sha512-Px/Hwhhyk2PubCA4ZaRFsfvwxhbxXsetJyvqC6aFFi8WhJhA+oVC33aTzuAeWmM3fhb4/8ce8YsHXI1d6ChcKg==} + '@react-grab/cli@0.2.0': + resolution: {integrity: sha512-LVfA+j5cFT0Szd958WRD8W/B8ucCtz4ViiveDKWLqAwBkZFW9VRW1ZvwOUhp/M3SmVPtjDaRkW12GvAuTArP+Q==, tarball: https://registry.npmjs.org/@react-grab/cli/-/cli-0.2.0.tgz} hasBin: true '@reactour/mask@1.2.0': @@ -2141,90 +2154,96 @@ packages: react: 16.x || 17.x || 18.x || 19.x '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==, tarball: https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==, tarball: https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==, tarball: https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==, tarball: https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2245,127 +2264,140 @@ packages: optional: true '@rollup/rollup-android-arm-eabi@4.61.1': - resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz} cpu: [arm] os: [android] '@rollup/rollup-android-arm64@4.61.1': - resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz} cpu: [arm64] os: [android] '@rollup/rollup-darwin-arm64@4.61.1': - resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz} cpu: [arm64] os: [darwin] '@rollup/rollup-darwin-x64@4.61.1': - resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz} cpu: [x64] os: [darwin] '@rollup/rollup-freebsd-arm64@4.61.1': - resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz} cpu: [arm64] os: [freebsd] '@rollup/rollup-freebsd-x64@4.61.1': - resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz} cpu: [x64] os: [freebsd] '@rollup/rollup-linux-arm-gnueabihf@4.61.1': - resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.61.1': - resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.61.1': - resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.61.1': - resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.61.1': - resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.61.1': - resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.61.1': - resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.61.1': - resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.61.1': - resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.61.1': - resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.61.1': - resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.61.1': - resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.61.1': - resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.61.1': - resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==, tarball: https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz} cpu: [x64] os: [openbsd] '@rollup/rollup-openharmony-arm64@4.61.1': - resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==, tarball: https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz} cpu: [arm64] os: [openharmony] '@rollup/rollup-win32-arm64-msvc@4.61.1': - resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz} cpu: [arm64] os: [win32] '@rollup/rollup-win32-ia32-msvc@4.61.1': - resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz} cpu: [ia32] os: [win32] '@rollup/rollup-win32-x64-gnu@4.61.1': - resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz} cpu: [x64] os: [win32] '@rollup/rollup-win32-x64-msvc@4.61.1': - resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz} cpu: [x64] os: [win32] @@ -2375,15 +2407,15 @@ packages: react: '>=16.8.0' '@sentry/conventions@0.16.0': - resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==, tarball: https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz} engines: {node: '>=14'} '@sentry/core@10.69.0': - resolution: {integrity: sha512-+uuqVEeiDzYuAKjZLqsROKXvRTbl/QeH0gfGRtpYib1cud4rAFWRIkFmcR7Jb7JGFYwmReyQotiTj/hcDszTZg==} + resolution: {integrity: sha512-+uuqVEeiDzYuAKjZLqsROKXvRTbl/QeH0gfGRtpYib1cud4rAFWRIkFmcR7Jb7JGFYwmReyQotiTj/hcDszTZg==, tarball: https://registry.npmjs.org/@sentry/core/-/core-10.69.0.tgz} engines: {node: '>=18'} '@sentry/node-core@10.69.0': - resolution: {integrity: sha512-IgArHczrZJxkgxoffHscj0NxQrG6kCazgmGQnlf3j58J1ec21YaUu8Tu+7G4Lo5tCiW3teQnwlKW1ttMXSqWRw==} + resolution: {integrity: sha512-IgArHczrZJxkgxoffHscj0NxQrG6kCazgmGQnlf3j58J1ec21YaUu8Tu+7G4Lo5tCiW3teQnwlKW1ttMXSqWRw==, tarball: https://registry.npmjs.org/@sentry/node-core/-/node-core-10.69.0.tgz} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -2404,11 +2436,11 @@ packages: optional: true '@sentry/node@10.69.0': - resolution: {integrity: sha512-xEXA1YGIiTZbrW6MWV34uS6JGQuQg2ijTI0zed+FsJb9JZKPYel/GZK8Km26vfTVb+yCXFmWZNBesKegNcVdzg==} + resolution: {integrity: sha512-xEXA1YGIiTZbrW6MWV34uS6JGQuQg2ijTI0zed+FsJb9JZKPYel/GZK8Km26vfTVb+yCXFmWZNBesKegNcVdzg==, tarball: https://registry.npmjs.org/@sentry/node/-/node-10.69.0.tgz} engines: {node: '>=18'} '@sentry/opentelemetry@10.69.0': - resolution: {integrity: sha512-3FyWV6YcEJuvLrlaKGE1dHXCI+1YO0a62w7PkwlRg8yp6K6YXkmdwu9GjqaYD+Ju4tm7uC7mHIsGFQMm0M7pqQ==} + resolution: {integrity: sha512-3FyWV6YcEJuvLrlaKGE1dHXCI+1YO0a62w7PkwlRg8yp6K6YXkmdwu9GjqaYD+Ju4tm7uC7mHIsGFQMm0M7pqQ==, tarball: https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.69.0.tgz} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -2416,11 +2448,11 @@ packages: '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 '@sentry/server-utils@10.69.0': - resolution: {integrity: sha512-0MwHrA8+nNvMIsqf8m3cXwCBlUjr6AS7N6CZvHJtY1DkqEvQqEbD5VIrhzEyHN/KMZIgQ8XeDCQRhjnXFQGRhg==} + resolution: {integrity: sha512-0MwHrA8+nNvMIsqf8m3cXwCBlUjr6AS7N6CZvHJtY1DkqEvQqEbD5VIrhzEyHN/KMZIgQ8XeDCQRhjnXFQGRhg==, tarball: https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.69.0.tgz} engines: {node: '>=18'} '@shaderfrog/glsl-parser@7.0.1': - resolution: {integrity: sha512-8mpfsoPeRhesY3pOrzNZBL8uG6N5GVX1EHLBYbd4gzKs+c7vaEIqpTNK5VrffU33qQN4cwpP2v3u4aPPBU32sw==} + resolution: {integrity: sha512-8mpfsoPeRhesY3pOrzNZBL8uG6N5GVX1EHLBYbd4gzKs+c7vaEIqpTNK5VrffU33qQN4cwpP2v3u4aPPBU32sw==, tarball: https://registry.npmjs.org/@shaderfrog/glsl-parser/-/glsl-parser-7.0.1.tgz} engines: {node: '>=16'} '@standard-schema/spec@1.1.0': @@ -2430,61 +2462,65 @@ packages: resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} '@tailwindcss/oxide-android-arm64@4.3.3': - resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz} engines: {node: '>= 20'} cpu: [arm64] os: [android] '@tailwindcss/oxide-darwin-arm64@4.3.3': - resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] '@tailwindcss/oxide-darwin-x64@4.3.3': - resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz} engines: {node: '>= 20'} cpu: [x64] os: [darwin] '@tailwindcss/oxide-freebsd-x64@4.3.3': - resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz} engines: {node: '>= 20'} cpu: [arm] os: [linux] '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.3': - resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.3': - resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -2496,13 +2532,13 @@ packages: - tslib '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz} engines: {node: '>= 20'} cpu: [arm64] os: [win32] '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz} engines: {node: '>= 20'} cpu: [x64] os: [win32] @@ -2623,7 +2659,7 @@ packages: '@testing-library/dom': '>=7.21.4' '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==, tarball: https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2671,7 +2707,7 @@ packages: resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==, tarball: https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz} '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2775,7 +2811,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/types@8.66.0': - resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.65.0': @@ -2884,11 +2920,11 @@ packages: hasBin: true agent-install@0.0.5: - resolution: {integrity: sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==} + resolution: {integrity: sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==, tarball: https://registry.npmjs.org/agent-install/-/agent-install-0.0.5.tgz} hasBin: true agent-install@0.0.6: - resolution: {integrity: sha512-7NRMZ/ZDz2vHevQTgJsocBFpakB1/Wx5ip19YSJuj4VOXpraWztTerViNtdSyARKZT9e2yVwUUB5JXXCE7mNrA==} + resolution: {integrity: sha512-7NRMZ/ZDz2vHevQTgJsocBFpakB1/Wx5ip19YSJuj4VOXpraWztTerViNtdSyARKZT9e2yVwUUB5JXXCE7mNrA==, tarball: https://registry.npmjs.org/agent-install/-/agent-install-0.0.6.tgz} hasBin: true ajv-formats@3.0.1: @@ -2914,7 +2950,7 @@ packages: engines: {node: '>=8'} ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==, tarball: https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz} engines: {node: '>=12'} ansi-styles@3.2.1: @@ -2995,7 +3031,7 @@ packages: resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} astring@1.9.0: - resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==, tarball: https://registry.npmjs.org/astring/-/astring-1.9.0.tgz} hasBin: true async-function@1.0.0: @@ -3009,7 +3045,7 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} atomically@2.1.1: - resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} + resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==, tarball: https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz} available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} @@ -3042,7 +3078,7 @@ packages: react: '>=17.0.1' bippy@0.6.1: - resolution: {integrity: sha512-ky4m94Y/KfsddjGkKTsV4uFjZqkJjpOjQ2t5gKPdX6XH1MNxMNX5FrVefsxV4lpjemEmEdwe0e0YbzAMNs3oUQ==} + resolution: {integrity: sha512-ky4m94Y/KfsddjGkKTsV4uFjZqkJjpOjQ2t5gKPdX6XH1MNxMNX5FrVefsxV4lpjemEmEdwe0e0YbzAMNs3oUQ==, tarball: https://registry.npmjs.org/bippy/-/bippy-0.6.1.tgz} peerDependencies: react: '>=17.0.1' @@ -3058,7 +3094,7 @@ packages: engines: {node: 18 || 20 || >=22} brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz} engines: {node: 20 || >=22} braces@3.0.3: @@ -3140,7 +3176,7 @@ packages: engines: {node: '>=10'} chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==, tarball: https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} character-entities-html4@2.1.0: @@ -3160,7 +3196,7 @@ packages: engines: {node: '>= 20.19.0'} cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==, tarball: https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz} class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3169,11 +3205,11 @@ packages: resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==, tarball: https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz} engines: {node: '>=18'} cli-spinners@3.4.0: - resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==, tarball: https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz} engines: {node: '>=18.20'} clsx@2.1.1: @@ -3248,7 +3284,7 @@ packages: engines: {'0': node >= 6.0} conf@15.1.0: - resolution: {integrity: sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==} + resolution: {integrity: sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==, tarball: https://registry.npmjs.org/conf/-/conf-15.1.0.tgz} engines: {node: '>=20'} confbox@0.2.4: @@ -3362,7 +3398,7 @@ packages: resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} debounce-fn@6.0.0: - resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} + resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==, tarball: https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz} engines: {node: '>=18'} debug@4.4.3: @@ -3436,7 +3472,7 @@ packages: engines: {node: '>=6'} deslop-js@0.9.12: - resolution: {integrity: sha512-Ku6Zngzmu4EISb58WUkRKZytfgWjJ18Cic7lb4wl+/BF0tHWRvpBOLlA0FP35r82mV45Y72AK3RPC1Nw0GLbIw==} + resolution: {integrity: sha512-Ku6Zngzmu4EISb58WUkRKZytfgWjJ18Cic7lb4wl+/BF0tHWRvpBOLlA0FP35r82mV45Y72AK3RPC1Nw0GLbIw==, tarball: https://registry.npmjs.org/deslop-js/-/deslop-js-0.9.12.tgz} destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -3478,7 +3514,7 @@ packages: resolution: {integrity: sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==} dot-prop@10.2.0: - resolution: {integrity: sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==} + resolution: {integrity: sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==, tarball: https://registry.npmjs.org/dot-prop/-/dot-prop-10.2.0.tgz} engines: {node: '>=20'} dotenv@17.4.2: @@ -3518,7 +3554,7 @@ packages: engines: {node: '>=20.19.0'} env-paths@3.0.0: - resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==, tarball: https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} error-ex@1.3.4: @@ -3602,7 +3638,7 @@ packages: eslint: '>=7' eslint-plugin-react-hooks@7.1.1: - resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==, tarball: https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 @@ -3623,7 +3659,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==, tarball: https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: @@ -3828,12 +3864,12 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, tarball: https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, tarball: https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] @@ -3860,7 +3896,7 @@ packages: engines: {node: '>=6.9.0'} get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==, tarball: https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz} engines: {node: '>=18'} get-intrinsic@1.3.0: @@ -4033,7 +4069,7 @@ packages: engines: {node: '>=6'} import-in-the-middle@3.3.3: - resolution: {integrity: sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==} + resolution: {integrity: sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==, tarball: https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz} engines: {node: '>=18'} imurmurhash@0.1.4: @@ -4160,7 +4196,7 @@ packages: engines: {node: '>=18'} is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==, tarball: https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz} engines: {node: '>=12'} is-map@2.0.3: @@ -4222,7 +4258,7 @@ packages: engines: {node: '>= 0.4'} is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==, tarball: https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz} engines: {node: '>=18'} is-weakmap@2.0.2: @@ -4336,7 +4372,7 @@ packages: hasBin: true jsonc-parser@3.3.1: - resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==, tarball: https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz} jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -4372,133 +4408,141 @@ packages: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, tarball: https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==, tarball: https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, tarball: https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==, tarball: https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, tarball: https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==, tarball: https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, tarball: https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==, tarball: https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, tarball: https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==, tarball: https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, tarball: https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==, tarball: https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, tarball: https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==, tarball: https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] @@ -4532,7 +4576,7 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} log-symbols@7.0.1: - resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==, tarball: https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz} engines: {node: '>=18'} longest-streak@3.1.0: @@ -4565,7 +4609,7 @@ packages: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} magicast@0.5.4: - resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==, tarball: https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz} make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} @@ -4660,7 +4704,7 @@ packages: engines: {node: '>= 8'} meriyah@6.1.4: - resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} + resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==, tarball: https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz} engines: {node: '>=18.0.0'} micromark-core-commonmark@2.0.3: @@ -4768,7 +4812,7 @@ packages: engines: {node: '>=18'} mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==, tarball: https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz} engines: {node: '>=18'} min-indent@1.0.1: @@ -4780,7 +4824,7 @@ packages: engines: {node: 18 || 20 || >=22} minimatch@10.2.6: - resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -4815,7 +4859,7 @@ packages: resolution: {integrity: sha512-syNcDdX3KT+Jq3je6eGjBhuc24Z68td2VG0zNFqRswaE433D9SNH5VRy/xrGbJsUixfppLLccXhAW9JSf6n+SQ==} module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==, tarball: https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz} monaco-editor@0.54.0: resolution: {integrity: sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==} @@ -4894,7 +4938,7 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==, tarball: https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz} engines: {node: '>=18'} open@11.0.0: @@ -4926,7 +4970,7 @@ packages: engines: {node: '>= 0.8.0'} ora@9.4.1: - resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==, tarball: https://registry.npmjs.org/ora/-/ora-9.4.1.tgz} engines: {node: '>=20'} own-keys@1.0.1: @@ -4938,18 +4982,18 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} oxc-parser@0.143.0: - resolution: {integrity: sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==} + resolution: {integrity: sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==, tarball: https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.143.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} oxc-resolver@11.24.2: resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} oxlint-plugin-react-doctor@0.9.12: - resolution: {integrity: sha512-BplcCUU/tGByFGgY1YIax6evUmjk0K8zOGGrdPs3A3dqamrzjUvVuJdYpM1Ftyt/B5fFx6+VwRrWi3YZbIz4VQ==} + resolution: {integrity: sha512-BplcCUU/tGByFGgY1YIax6evUmjk0K8zOGGrdPs3A3dqamrzjUvVuJdYpM1Ftyt/B5fFx6+VwRrWi3YZbIz4VQ==, tarball: https://registry.npmjs.org/oxlint-plugin-react-doctor/-/oxlint-plugin-react-doctor-0.9.12.tgz} engines: {node: ^20.19.0 || >=22.13.0} oxlint@1.77.0: - resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==} + resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==, tarball: https://registry.npmjs.org/oxlint/-/oxlint-1.77.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4982,7 +5026,7 @@ packages: engines: {node: '>=6'} package-manager-detector@1.8.0: - resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==, tarball: https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz} papaparse@5.5.4: resolution: {integrity: sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==} @@ -5178,7 +5222,7 @@ packages: optional: true react-doctor@0.9.12: - resolution: {integrity: sha512-H7RNg13RYKwpQvi3+O3IkSj8pAD1pGTxSetxu7aOwXX3wmF+BydCLDiWxkPT9Pq7bIMHjuJ0taSZLsxEjlNjcA==} + resolution: {integrity: sha512-H7RNg13RYKwpQvi3+O3IkSj8pAD1pGTxSetxu7aOwXX3wmF+BydCLDiWxkPT9Pq7bIMHjuJ0taSZLsxEjlNjcA==, tarball: https://registry.npmjs.org/react-doctor/-/react-doctor-0.9.12.tgz} engines: {node: ^20.19.0 || >=22.13.0} hasBin: true @@ -5192,8 +5236,8 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 - react-grab@0.1.50: - resolution: {integrity: sha512-zRkHKq/8a1msCpEOp8BDROeQZT50m0OH2XPrP6jk5op+JAHrlsm3pj7eAQMOsct87EZDeGNnu4r+sGsJJzyw1Q==} + react-grab@0.2.0: + resolution: {integrity: sha512-ohhsfXD4qN0j6cMQd56aaVJBPDF3kUdviF/Od1Eak/rEIXQ3svQ4gjkwTfuTZwTBnxHRL16p9JqdlVQO5nUHqA==, tarball: https://registry.npmjs.org/react-grab/-/react-grab-0.2.0.tgz} hasBin: true peerDependencies: react: '>=17.0.0' @@ -5333,7 +5377,7 @@ packages: engines: {node: '>=0.10.0'} require-in-the-middle@8.0.1: - resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==, tarball: https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} resize-observer-polyfill@1.5.1: @@ -5357,7 +5401,7 @@ packages: hasBin: true restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==, tarball: https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz} engines: {node: '>=18'} reusify@1.1.0: @@ -5370,7 +5414,7 @@ packages: hasBin: true rollup@4.61.1: - resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5420,7 +5464,7 @@ packages: resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} semifies@1.0.0: - resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} + resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==, tarball: https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz} semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} @@ -5505,7 +5549,7 @@ packages: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, tarball: https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz} engines: {node: '>=14'} sisteransi@1.0.5: @@ -5524,7 +5568,7 @@ packages: engines: {node: '>=0.10.0'} source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, tarball: https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz} engines: {node: '>=0.10.0'} space-separated-tokens@2.0.2: @@ -5565,7 +5609,7 @@ packages: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} stdin-discarder@0.3.2: - resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==, tarball: https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz} engines: {node: '>=18'} stop-iteration-iterator@1.1.0: @@ -5573,7 +5617,7 @@ packages: engines: {node: '>= 0.4'} string-width@8.2.2: - resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==, tarball: https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz} engines: {node: '>=20'} string.prototype.matchall@4.0.12: @@ -5605,7 +5649,7 @@ packages: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz} engines: {node: '>=12'} strip-bom@3.0.0: @@ -5629,10 +5673,10 @@ packages: engines: {node: '>=0.10.0'} stubborn-fs@2.0.0: - resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==, tarball: https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz} stubborn-utils@1.0.2: - resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==, tarball: https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz} style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -5660,7 +5704,7 @@ packages: engines: {node: '>=8.0.0'} tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==, tarball: https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz} engines: {node: '>=20'} tailwind-merge@3.6.0: @@ -5775,7 +5819,7 @@ packages: engines: {node: '>=8'} type-fest@5.8.0: - resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==, tarball: https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz} engines: {node: '>=20'} type-is@2.1.0: @@ -5822,7 +5866,7 @@ packages: engines: {node: '>=8'} uint8array-extras@1.5.0: - resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==, tarball: https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz} engines: {node: '>=18'} unbash@4.0.3: @@ -5867,7 +5911,7 @@ packages: engines: {node: '>= 0.8'} unplugin@3.0.0: - resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==, tarball: https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz} engines: {node: ^20.19.0 || >=22.12.0} update-browserslist-db@1.2.3: @@ -6014,24 +6058,24 @@ packages: optional: true vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==, tarball: https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz} engines: {node: '>=14.0.0'} vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==, tarball: https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz} vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==, tarball: https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz} vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==, tarball: https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz} vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==, tarball: https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz} hasBin: true vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==, tarball: https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz} w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} @@ -6062,7 +6106,7 @@ packages: engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} when-exit@2.1.5: - resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==, tarball: https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz} which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} @@ -6126,7 +6170,7 @@ packages: optional: true ws@8.21.2: - resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==} + resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==, tarball: https://registry.npmjs.org/ws/-/ws-8.21.2.tgz} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6169,11 +6213,11 @@ packages: engines: {node: '>=10'} yoctocolors@2.2.0: - resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==, tarball: https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz} engines: {node: '>=18'} yoga-layout@3.2.1: - resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==, tarball: https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz} zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} @@ -6187,7 +6231,7 @@ packages: zod: ^3.24.4 zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==, tarball: https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz} engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -6663,24 +6707,6 @@ snapshots: '@date-fns/tz@1.4.1': {} - '@dnd-kit/accessibility@3.1.1(react@19.2.8)': - dependencies: - react: 19.2.8 - tslib: 2.8.1 - - '@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@dnd-kit/accessibility': 3.1.1(react@19.2.8) - '@dnd-kit/utilities': 3.2.2(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - tslib: 2.8.1 - - '@dnd-kit/utilities@3.2.2(react@19.2.8)': - dependencies: - react: 19.2.8 - tslib: 2.8.1 - '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -7953,7 +7979,7 @@ snapshots: '@radix-ui/rect@1.1.2': {} - '@react-grab/cli@0.1.50': + '@react-grab/cli@0.2.0': dependencies: agent-install: 0.0.6 commander: 14.0.3 @@ -11491,9 +11517,9 @@ snapshots: dependencies: react: 19.2.8 - react-grab@0.1.50(react@19.2.8): + react-grab@0.2.0(react@19.2.8): dependencies: - '@react-grab/cli': 0.1.50 + '@react-grab/cli': 0.2.0 bippy: 0.6.1(react@19.2.8) optionalDependencies: react: 19.2.8 @@ -11559,7 +11585,7 @@ snapshots: react: 19.2.8 react-doctor: 0.9.12(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(eslint@9.39.4(jiti@2.7.0)) react-dom: 19.2.8(react@19.2.8) - react-grab: 0.1.50(react@19.2.8) + react-grab: 0.2.0(react@19.2.8) optionalDependencies: esbuild: 0.28.1 unplugin: 3.0.0 diff --git a/src/components/Editor/Context/PipelineDetails.tsx b/src/components/Editor/Context/PipelineDetails.tsx deleted file mode 100644 index 10205fff14..0000000000 --- a/src/components/Editor/Context/PipelineDetails.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useEffect, useState } from "react"; - -import { useValidationIssueNavigation } from "@/components/Editor/hooks/useValidationIssueNavigation"; -import { ViewYamlButton } from "@/components/shared/Buttons/ViewYamlButton"; -import { ActionBlock } from "@/components/shared/ContextPanel/Blocks/ActionBlock"; -import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock"; -import { KeyValueList } from "@/components/shared/ContextPanel/Blocks/KeyValueList"; -import { TextBlock } from "@/components/shared/ContextPanel/Blocks/TextBlock"; -import { CopyText } from "@/components/shared/CopyText/CopyText"; -import { FavoriteToggle } from "@/components/shared/FavoriteToggle"; -import { PipelineDescription } from "@/components/shared/PipelineDescription/PipelineDescription"; -import { PipelineRunNameTemplateEditor } from "@/components/shared/PipelineRunNameTemplate/PipelineRunNameTemplateEditor"; -import { useFlagValue } from "@/components/shared/Settings/useFlags"; -import { BlockStack } from "@/components/ui/layout"; -import useToastNotification from "@/hooks/useToastNotification"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { SYSTEM_ANNOTATIONS } from "@/utils/annotations"; -import { getComponentFileFromList } from "@/utils/componentStore"; -import { USER_PIPELINES_LIST_NAME } from "@/utils/constants"; -import { tracking } from "@/utils/tracking"; - -import PipelineIO from "../../shared/Execution/PipelineIO"; -import { PipelineNotesEditor } from "./PipelineNotesEditor"; -import { PipelineValidationList } from "./PipelineValidationList"; -import RenamePipeline from "./RenamePipeline"; -import { PipelineTags } from "./Tags/PipelineTags"; - -const PipelineDetails = () => { - const notify = useToastNotification(); - const { componentSpec, digest, globalValidationIssues } = useComponentSpec(); - - const templatizedRunNameEnabled = useFlagValue( - "templatized-pipeline-run-name", - ); - - const { handleIssueClick, groupedIssues } = useValidationIssueNavigation( - globalValidationIssues, - ); - - // State for file metadata - const [fileMeta, setFileMeta] = useState<{ - creationTime?: Date; - modificationTime?: Date; - createdBy?: string; - }>({}); - - // Fetch file metadata on mount or when componentSpec.name changes - useEffect(() => { - const fetchMeta = async () => { - if (!componentSpec.name) return; - - try { - const file = await getComponentFileFromList( - USER_PIPELINES_LIST_NAME, - componentSpec.name, - ); - if (file) { - setFileMeta({ - creationTime: file.creationTime, - modificationTime: file.modificationTime, - createdBy: file.componentRef.spec.metadata?.annotations?.author, - }); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - notify("Failed to fetch file metadata: " + message, "error"); - } - }; - fetchMeta(); - }, [componentSpec.name]); - - const metadata = [ - { - label: "Created by", - value: fileMeta.createdBy, - }, - { - label: "Created at", - value: fileMeta.creationTime?.toLocaleString(), - }, - { - label: "Last updated", - value: fileMeta.modificationTime?.toLocaleString(), - }, - ]; - - const annotations = Object.entries(componentSpec.metadata?.annotations || {}) - .filter(([key]) => !SYSTEM_ANNOTATIONS.includes(key)) - .map(([key, value]) => ({ - label: key, - value: String(value), - })); - - const actions = [ - , - , - ...(componentSpec.name - ? [ - , - ] - : []), - ]; - - return ( - - - {componentSpec.name ?? "Unnamed Pipeline"} - - - - - - - - - {templatizedRunNameEnabled && ( - - - - )} - - {digest && ( - - )} - - {annotations.length > 0 && ( - - )} - - - - - - - - - - - - - - - - ); -}; - -export default PipelineDetails; diff --git a/src/components/Editor/Context/PipelineNotesEditor.tsx b/src/components/Editor/Context/PipelineNotesEditor.tsx deleted file mode 100644 index 80bce027ed..0000000000 --- a/src/components/Editor/Context/PipelineNotesEditor.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; - -import { Textarea } from "@/components/ui/textarea"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { - getAnnotationValue, - PIPELINE_NOTES_ANNOTATION, - setComponentSpecAnnotation, -} from "@/utils/annotations"; -import { debounce } from "@/utils/debounce"; - -const TRACK_DEBOUNCE_MS = 100; - -export const PipelineNotesEditor = () => { - const { componentSpec, setComponentSpec } = useComponentSpec(); - const { track } = useAnalytics(); - - // eslint-disable-next-line no-restricted-syntax -- debounce() is stateful; compiler cannot prove purity - const debouncedTrack = useMemo( - () => - debounce(() => { - track("pipeline_editor.configuration_panel.text_field_edited", { - field: "notes", - }); - }, TRACK_DEBOUNCE_MS), - [track], - ); - - useEffect(() => () => debouncedTrack.cancel(), [debouncedTrack]); - - const annotations = componentSpec.metadata?.annotations; - const notes = - getAnnotationValue(annotations, PIPELINE_NOTES_ANNOTATION) ?? ""; - - const [value, setValue] = useState(notes); - - const onBlur = () => { - debouncedTrack(); - setComponentSpec( - setComponentSpecAnnotation( - componentSpec, - PIPELINE_NOTES_ANNOTATION, - value, - ), - ); - }; - - return ( - setValue(e.target.value)} - onBlur={onBlur} - placeholder="Share context about this pipeline..." - className="text-xs" - /> - ); -}; diff --git a/src/components/Editor/Context/PipelineValidationList.tsx b/src/components/Editor/Context/PipelineValidationList.tsx deleted file mode 100644 index 226d2c42bc..0000000000 --- a/src/components/Editor/Context/PipelineValidationList.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { useState } from "react"; - -import { InfoBox } from "@/components/shared/InfoBox"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Paragraph, Text } from "@/components/ui/typography"; -import { cn } from "@/lib/utils"; -import { pluralize } from "@/utils/string"; -import { - type ComponentValidationIssue, - isFixableIssue, -} from "@/utils/validations"; - -import type { ValidationIssueGroup } from "../hooks/useValidationIssueNavigation"; - -interface PipelineValidationListProps { - groupedIssues: ValidationIssueGroup[]; - globalValidationIssues: ComponentValidationIssue[]; - onIssueSelect: (issue: ComponentValidationIssue) => void; -} - -const levelToTone = ( - level: ValidationIssueGroup["issues"][number]["level"], -): "critical" | "warning" => { - if (level === "error") { - return "critical"; - } - return "warning"; -}; - -const isGroupWarningOnly = (group: ValidationIssueGroup): boolean => { - return group.issues.every((issue) => issue.level === "warning"); -}; - -const groupTriggerWarningStyles = cn( - "w-full justify-start gap-2 rounded-lg font-medium", - "bg-warning/10 text-warning-foreground hover:bg-warning/15", -); - -const groupTriggerErrorStyles = cn( - "w-full justify-start gap-2 rounded-lg font-medium", - "bg-destructive/10 text-destructive-foreground hover:bg-destructive/15", -); - -const warningButtonStyles = cn( - "h-auto w-full flex-col items-start justify-start gap-1 rounded-lg", - "whitespace-normal border-warning/20 bg-warning/5 px-3 py-2 text-left", - "hover:border-warning/80 hover:bg-warning/15", - "focus-visible:ring-warning/40", -); - -const errorButtonStyles = cn( - "h-auto w-full flex-col items-start justify-start gap-1 rounded-lg", - "whitespace-normal border-destructive/20 bg-destructive/5 px-3 py-2 text-left", - "hover:border-destructive/40 hover:bg-destructive/10", - "focus-visible:ring-destructive/40", -); - -export const PipelineValidationList = ({ - globalValidationIssues, - groupedIssues, - onIssueSelect, -}: PipelineValidationListProps) => { - const totalIssueCount = globalValidationIssues.length; - const fixableIssueCount = - globalValidationIssues.filter(isFixableIssue).length; - const nonFixableIssueCount = totalIssueCount - fixableIssueCount; - - const isValid = totalIssueCount === 0; - - const [openGroups, setOpenGroups] = useState>(new Set()); - - const toggleGroup = (pathKey: string) => { - setOpenGroups((prev) => { - const next = new Set(prev); - if (next.has(pathKey)) { - next.delete(pathKey); - } else { - next.add(pathKey); - } - return next; - }); - }; - - if (isValid) { - return ( - - Pipeline is ready for submission - - ); - } - - const errorTitle = - nonFixableIssueCount > 0 - ? `${nonFixableIssueCount} ${pluralize(nonFixableIssueCount, "error")}` - : ""; - const warningTitle = - fixableIssueCount > 0 - ? `${fixableIssueCount} ${pluralize(fixableIssueCount, "warning")}` - : ""; - const totalIssuesTitle = [errorTitle, warningTitle] - .filter(Boolean) - .join(" and "); - - return ( - 0 ? "error" : "warning"} - title={`${totalIssuesTitle} detected`} - > - - Select an item to jump to its location in the pipeline. - - - - {groupedIssues.map((group) => { - const isOpen = openGroups.has(group.pathKey); - const isWarningOnly = isGroupWarningOnly(group); - - return ( - toggleGroup(group.pathKey)} - className="w-full" - data-testid="validation-group" - > - - - - - - - {group.depth === 0 - ? "Root Pipeline" - : `Subgraph: ${group.pathLabel}`} - - - {group.issues.length} - - - - - - {group.fullPath} - - - - - - {group.issues.map( - ({ - issue, - displayName, - typeLabel, - displayMessage, - level, - }) => ( - onIssueSelect(issue)} - data-testid="validation-issue" - data-issue-level={level} - className={ - level === "error" - ? errorButtonStyles - : warningButtonStyles - } - > - - - {typeLabel} - - - • - - - {displayName} - - - - {displayMessage} - - - ), - )} - - - - ); - })} - - - ); -}; diff --git a/src/components/Editor/Context/RenamePipeline.tsx b/src/components/Editor/Context/RenamePipeline.tsx deleted file mode 100644 index 16ae63327b..0000000000 --- a/src/components/Editor/Context/RenamePipeline.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useLocation, useNavigate } from "@tanstack/react-router"; -import { Edit3 } from "lucide-react"; - -import TooltipButton from "@/components/shared/Buttons/TooltipButton"; -import { PipelineNameDialog } from "@/components/shared/Dialogs"; -import useToastNotification from "@/hooks/useToastNotification"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { APP_ROUTES } from "@/routes/router"; -import { renameComponentFileInList } from "@/utils/componentStore"; -import { USER_PIPELINES_LIST_NAME } from "@/utils/constants"; -import { tracking } from "@/utils/tracking"; - -const RenamePipeline = () => { - const { componentSpec, saveComponentSpec } = useComponentSpec(); - const notify = useToastNotification(); - const { track } = useAnalytics(); - const navigate = useNavigate(); - - const location = useLocation(); - const pathname = location.pathname; - - const title = componentSpec?.name; - - const isSubmitDisabled = (name: string) => { - return name === title; - }; - - const handleTitleUpdate = async (name: string) => { - if (!componentSpec) { - notify("Update failed: ComponentSpec not found", "error"); - return; - } - - await renameComponentFileInList( - USER_PIPELINES_LIST_NAME, - title ?? "", - name, - pathname, - ); - - await saveComponentSpec(name); - - const urlName = encodeURIComponent(name); - const url = APP_ROUTES.PIPELINE_EDITOR.replace("$name", urlName); - - navigate({ to: url }); - }; - - return ( - - - - } - title="Name Pipeline" - description="Unsaved pipeline changes will be lost." - initialName={title ?? ""} - onSubmit={handleTitleUpdate} - submitButtonText="Update Title" - isSubmitDisabled={isSubmitDisabled} - excludeNames={title ? [title] : undefined} - onOpenChange={(open) => { - if (open) track("pipeline_editor.name_pipeline_dialog_impression"); - }} - /> - ); -}; - -export default RenamePipeline; diff --git a/src/components/Editor/Context/Tags/PipelineTags.tsx b/src/components/Editor/Context/Tags/PipelineTags.tsx deleted file mode 100644 index 83342d19dc..0000000000 --- a/src/components/Editor/Context/Tags/PipelineTags.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Paragraph } from "@/components/ui/typography"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { - getPipelineTagsFromSpec, - PIPELINE_TAGS_ANNOTATION, - setComponentSpecAnnotation, -} from "@/utils/annotations"; - -import { Tag } from "./Tag"; -import { TagEditor } from "./TagEditor"; - -const TAG_LIMIT = 10; - -export const PipelineTags = () => { - const { componentSpec, setComponentSpec } = useComponentSpec(); - const { track } = useAnalytics(); - - const [isAdding, setIsAdding] = useState(false); - const [newTagValue, setNewTagValue] = useState(""); - const [editingIndex, setEditingIndex] = useState(null); - const [editValue, setEditValue] = useState(""); - - const tags = getPipelineTagsFromSpec(componentSpec); - - const saveTags = (updatedTags: string[]) => { - const tagsString = updatedTags.filter(Boolean).join(","); - setComponentSpec( - setComponentSpecAnnotation( - componentSpec, - PIPELINE_TAGS_ANNOTATION, - tagsString, - ), - ); - }; - - const handleAddTag = () => { - const trimmedTag = newTagValue.trim(); - if (trimmedTag && !tags.includes(trimmedTag) && tags.length < TAG_LIMIT) { - track("pipeline_editor.configuration_panel.tag_added"); - saveTags([...tags, trimmedTag]); - } - setNewTagValue(""); - setIsAdding(false); - }; - - const handleCancelAdd = () => { - setNewTagValue(""); - setIsAdding(false); - }; - - const handleRemoveTag = (index: number) => { - track("pipeline_editor.configuration_panel.tag_removed"); - saveTags(tags.filter((_, i) => i !== index)); - }; - - const handleStartEdit = (index: number) => { - setEditingIndex(index); - setEditValue(tags[index]); - }; - - const handleSaveEdit = () => { - if (editingIndex !== null && editValue.trim()) { - const trimmedValue = editValue.trim(); - const isDuplicate = tags.some( - (tag, idx) => tag === trimmedValue && idx !== editingIndex, - ); - if (!isDuplicate) { - const updatedTags = [...tags]; - updatedTags[editingIndex] = trimmedValue; - saveTags(updatedTags); - } - } - setEditingIndex(null); - setEditValue(""); - }; - - const handleCancelEdit = () => { - setEditingIndex(null); - setEditValue(""); - }; - - return ( - - {tags.length >= TAG_LIMIT ? ( - - Tag limit reached ({TAG_LIMIT}) - - ) : isAdding ? ( - - ) : ( - setIsAdding(true)} - className="my-0.5" - > - - Add Tag - - )} - - {tags.length > 0 && ( - - {tags.map((tag, index) => - editingIndex === index ? ( - - ) : ( - handleStartEdit(index)} - onRemove={() => handleRemoveTag(index)} - /> - ), - )} - - )} - - ); -}; diff --git a/src/components/Editor/PipelineEditor.tsx b/src/components/Editor/PipelineEditor.tsx deleted file mode 100644 index 59dc4a0c94..0000000000 --- a/src/components/Editor/PipelineEditor.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import "@/styles/editor.css"; - -import { Background, MiniMap, type ReactFlowProps } from "@xyflow/react"; -import { useRef, useState } from "react"; - -import { ComponentEditorProvider } from "@/components/shared/ComponentEditor/ComponentEditorProvider"; -import { CollapsibleContextPanel } from "@/components/shared/ContextPanel/CollapsibleContextPanel"; -import { - FlowCanvas, - FlowControls, - FlowSidebar, -} from "@/components/shared/ReactFlow"; -import { UndoRedo } from "@/components/shared/UndoRedo"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { AutoSaveProvider } from "@/providers/AutoSaveProvider"; -import { ComponentLibraryProvider } from "@/providers/ComponentLibraryProvider"; -import { ForcedSearchProvider } from "@/providers/ComponentLibraryProvider/ForcedSearchProvider"; -import { - EMPTY_GRAPH_COMPONENT_SPEC, - useComponentSpec, -} from "@/providers/ComponentSpecProvider"; -import { ContextPanelProvider } from "@/providers/ContextPanelProvider"; - -import { LoadingScreen } from "../shared/LoadingScreen"; -import type { FlowCanvasRef } from "../shared/ReactFlow/FlowCanvas/FlowCanvas"; -import type { LayoutAlgorithm } from "../shared/ReactFlow/FlowCanvas/utils/autolayout"; -import { NodesOverlayProvider } from "../shared/ReactFlow/NodesOverlay/NodesOverlayProvider"; -import PipelineDetails from "./Context/PipelineDetails"; - -const GRID_SIZE = 10; - -const PipelineEditor = () => { - const flowCanvasRef = useRef(null); - - const { componentSpec, isLoading } = useComponentSpec(); - - const [flowConfig, setFlowConfig] = useState({ - snapGrid: [GRID_SIZE, GRID_SIZE], - snapToGrid: true, - panOnDrag: true, - selectionOnDrag: false, - nodesDraggable: true, - }); - - const updateFlowConfig = (updatedConfig: Partial) => { - setFlowConfig((prevConfig) => ({ - ...prevConfig, - ...updatedConfig, - })); - }; - - const handleAutoLayout = (algorithm: LayoutAlgorithm) => { - flowCanvasRef.current?.autoLayout(algorithm); - }; - - // If the pipeline is loading or the component spec is empty, show a loading spinner - if (isLoading || componentSpec === EMPTY_GRAPH_COMPONENT_SPEC) { - return ; - } - - return ( - - - }> - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default PipelineEditor; diff --git a/src/components/Editor/hooks/useValidationIssueNavigation.ts b/src/components/Editor/hooks/useValidationIssueNavigation.ts deleted file mode 100644 index 6e7320ca33..0000000000 --- a/src/components/Editor/hooks/useValidationIssueNavigation.ts +++ /dev/null @@ -1,169 +0,0 @@ -import equal from "fast-deep-equal"; -import { useEffect, useRef, useState } from "react"; - -import { useNodesOverlay } from "@/components/shared/ReactFlow/NodesOverlay/NodesOverlayProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { - inputNameToNodeId, - outputNameToNodeId, - taskIdToNodeId, -} from "@/utils/nodes/nodeIdUtils"; -import { - type ComponentValidationIssue, - isFixableIssue, -} from "@/utils/validations"; - -const ISSUE_TYPE_LABELS: Record = { - graph: "Graph", - task: "Task", - input: "Input", - argument: "Argument", - output: "Output", -}; - -type IssueLevel = "error" | "warning"; - -const getIssueLevel = (issue: ComponentValidationIssue): IssueLevel => { - if (isFixableIssue(issue)) { - return "warning"; - } - return "error"; -}; - -const getIssueNodeId = (issue: ComponentValidationIssue): string | null => { - if (issue.taskId) return taskIdToNodeId(issue.taskId); - if (issue.inputName) return inputNameToNodeId(issue.inputName); - if (issue.outputName) return outputNameToNodeId(issue.outputName); - return null; -}; - -interface ValidationIssueListItem { - issue: ComponentValidationIssue; - displayName: string; - displayMessage: string; - typeLabel: string; - level: IssueLevel; -} - -export interface ValidationIssueGroup { - pathKey: string; - pathLabel: string; - fullPath: string; - depth: number; - issues: ValidationIssueListItem[]; -} - -const groupIssuesByPath = ( - items: ValidationIssueListItem[], -): ValidationIssueGroup[] => { - const groupsMap = items.reduce((acc, item) => { - const pathKey = item.issue.subgraphPath.join(" > "); - const existing = acc.get(pathKey); - return acc.set(pathKey, existing ? [...existing, item] : [item]); - }, new Map()); - - return Array.from(groupsMap.entries()) - .map(([pathKey, groupItems]) => { - const { subgraphPath } = groupItems[0].issue; - const depth = subgraphPath.length - 1; - const isRoot = depth === 0; - const taskId = subgraphPath[subgraphPath.length - 1] ?? "Subgraph"; - - return { - pathKey, - pathLabel: isRoot ? "Root Pipeline" : taskId, - fullPath: isRoot ? "Root Pipeline" : subgraphPath.slice(1).join(" → "), - depth, - issues: groupItems, - }; - }) - .sort((a, b) => a.depth - b.depth); -}; - -export const useValidationIssueNavigation = ( - validationIssues: ComponentValidationIssue[], -) => { - const { currentSubgraphPath, navigateToPath } = useComponentSpec(); - const { fitNodeIntoView, notifyNode } = useNodesOverlay(); - - const [pendingIssue, setPendingIssue] = - useState(null); - - const highlightedNodeRef = useRef(null); - - const focusIssue = async (issue: ComponentValidationIssue) => { - const nodeId = getIssueNodeId(issue); - if (!nodeId) return; - - // Clear previous highlight - if (highlightedNodeRef.current && highlightedNodeRef.current !== nodeId) { - notifyNode(highlightedNodeRef.current, { type: "clear" }); - } - - await fitNodeIntoView(nodeId); - - if (issue.type === "task") { - highlightedNodeRef.current = nodeId; - notifyNode(nodeId, { type: "highlight" }); - } else { - highlightedNodeRef.current = null; - } - }; - - useEffect(() => { - return () => { - if (highlightedNodeRef.current) { - notifyNode(highlightedNodeRef.current, { type: "clear" }); - } - }; - }, [notifyNode]); - - const handleIssueClick = (issue: ComponentValidationIssue) => { - if (!equal(issue.subgraphPath, currentSubgraphPath)) { - setPendingIssue(issue); - navigateToPath(issue.subgraphPath); - return; - } - - focusIssue(issue); - }; - - useEffect(() => { - if (!pendingIssue) return; - if (!equal(pendingIssue.subgraphPath, currentSubgraphPath)) return; - - const frameId = requestAnimationFrame(() => { - focusIssue(pendingIssue); - setPendingIssue(null); - }); - - return () => cancelAnimationFrame(frameId); - }, [currentSubgraphPath, focusIssue, pendingIssue]); - - const issueItems: ValidationIssueListItem[] = validationIssues.map( - (issue) => { - const nodeLabel = - issue.taskId ?? issue.inputName ?? issue.outputName ?? null; - const fallbackName = - issue.subgraphPath.length <= 1 - ? "Pipeline" - : (issue.subgraphPath[issue.subgraphPath.length - 1] ?? "Pipeline"); - const displayName = nodeLabel ?? fallbackName; - - return { - issue, - displayName, - level: getIssueLevel(issue), - typeLabel: ISSUE_TYPE_LABELS[issue.type], - displayMessage: issue.message, - }; - }, - ); - - const groupedIssues = groupIssuesByPath(issueItems); - - return { - handleIssueClick, - groupedIssues, - }; -}; diff --git a/src/components/PipelineRun/PipelineRunPage.tsx b/src/components/PipelineRun/PipelineRunPage.tsx deleted file mode 100644 index 6756d6a6ad..0000000000 --- a/src/components/PipelineRun/PipelineRunPage.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Background, MiniMap, type ReactFlowProps } from "@xyflow/react"; -import { useCallback, useRef, useState } from "react"; - -import { FlowCanvas, FlowControls } from "@/components/shared/ReactFlow"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { ComponentLibraryProvider } from "@/providers/ComponentLibraryProvider"; -import { ContextPanelProvider } from "@/providers/ContextPanelProvider"; - -import { CollapsibleContextPanel } from "../shared/ContextPanel/CollapsibleContextPanel"; -import type { FlowCanvasRef } from "../shared/ReactFlow/FlowCanvas/FlowCanvas"; -import type { LayoutAlgorithm } from "../shared/ReactFlow/FlowCanvas/utils/autolayout"; -import { RunDetails } from "./RunDetails"; -import { RunToolbar } from "./RunToolbar"; -import { useFitNodeFromUrl } from "./useFitNodeFromUrl"; - -const GRID_SIZE = 10; - -const PipelineRunPage = () => { - const flowCanvasRef = useRef(null); - const { linkedNodeId } = useFitNodeFromUrl(); - - const [flowConfig, setFlowConfig] = useState({ - snapGrid: [GRID_SIZE, GRID_SIZE], - snapToGrid: true, - panOnDrag: true, - selectionOnDrag: false, - nodesDraggable: true, - }); - - const updateFlowConfig = useCallback( - (updatedConfig: Partial) => { - setFlowConfig((prevConfig) => ({ - ...prevConfig, - ...updatedConfig, - })); - }, - [], - ); - - const handleAutoLayout = (algorithm: LayoutAlgorithm) => { - flowCanvasRef.current?.autoLayout(algorithm); - }; - - return ( - }> - - - - - - - - - - - - - - - ); -}; - -export default PipelineRunPage; diff --git a/src/components/PipelineRun/components/InspectPipelineButton.test.tsx b/src/components/PipelineRun/components/InspectPipelineButton.test.tsx index 35a7717f9c..5200e67e34 100644 --- a/src/components/PipelineRun/components/InspectPipelineButton.test.tsx +++ b/src/components/PipelineRun/components/InspectPipelineButton.test.tsx @@ -16,6 +16,6 @@ describe("", () => { render(); const inspectButton = screen.getByTestId("inspect-pipeline-button"); act(() => fireEvent.click(inspectButton)); - expect(mockNavigate).toHaveBeenCalledWith({ to: "/editor-v2/foo" }); + expect(mockNavigate).toHaveBeenCalledWith({ to: "/editor/foo" }); }); }); diff --git a/src/components/PipelineRun/components/RerunPipelineButton.test.tsx b/src/components/PipelineRun/components/RerunPipelineButton.test.tsx index d732f6200d..b6ff67d162 100644 --- a/src/components/PipelineRun/components/RerunPipelineButton.test.tsx +++ b/src/components/PipelineRun/components/RerunPipelineButton.test.tsx @@ -186,7 +186,7 @@ describe("", () => { await waitFor(() => { expect(navigateMock).toHaveBeenCalledWith({ - to: "/runs-v2/123", + to: "/runs/123", }); }); }); diff --git a/src/components/PipelineRun/index.ts b/src/components/PipelineRun/index.ts deleted file mode 100644 index 19bcc22bed..0000000000 --- a/src/components/PipelineRun/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./PipelineRunPage"; diff --git a/src/components/PipelineRun/useFitNodeFromUrl.ts b/src/components/PipelineRun/useFitNodeFromUrl.ts deleted file mode 100644 index f08c664ca3..0000000000 --- a/src/components/PipelineRun/useFitNodeFromUrl.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect } from "react"; - -import { useNodesOverlay } from "@/components/shared/ReactFlow/NodesOverlay/NodesOverlayProvider"; - -/** - * Reads `?nodeId=` from the URL on mount, fits that node into view, selects it, - * and highlights it with an orange border. The highlight clears (and the param - * is removed from the URL) on the next user pointer or keyboard interaction. - */ -export const useFitNodeFromUrl = () => { - const { fitNodeIntoView, selectNode, notifyNode } = useNodesOverlay(); - - const linkedNodeId = new URLSearchParams(window.location.search).get( - "nodeId", - ); - - useEffect(() => { - if (!linkedNodeId) return; - - const nodeId = linkedNodeId; - let timeoutId: ReturnType; - let isHighlighted = false; - - const clearHighlight = () => { - if (!isHighlighted) return; - - isHighlighted = false; - - notifyNode(nodeId, { type: "clear" }); - - const url = new URL(window.location.href); - url.searchParams.delete("nodeId"); - - history.replaceState(null, "", url.toString()); - document.removeEventListener("pointerdown", clearHighlight); - document.removeEventListener("keydown", clearHighlight); - }; - - const focus = async () => { - const success = await fitNodeIntoView(nodeId); - if (!success) return; - - isHighlighted = true; - - selectNode(nodeId); - notifyNode(nodeId, { type: "highlight" }); - - timeoutId = setTimeout(() => { - document.addEventListener("pointerdown", clearHighlight); - document.addEventListener("keydown", clearHighlight); - }, 500); - }; - - // Double-RAF to allow the canvas to render nodes before querying them - const frameId = requestAnimationFrame(() => requestAnimationFrame(focus)); - - return () => { - cancelAnimationFrame(frameId); - clearTimeout(timeoutId); - clearHighlight(); - }; - }, []); - - return { linkedNodeId }; -}; diff --git a/src/components/layout/AppMenu.tsx b/src/components/layout/AppMenu.tsx index 195aa373bd..8c3af93997 100644 --- a/src/components/layout/AppMenu.tsx +++ b/src/components/layout/AppMenu.tsx @@ -10,9 +10,7 @@ import { OnboardingNavPill } from "@/components/Onboarding/OnboardingNavPill"; import { isAuthorizationRequired } from "@/components/shared/Authentication/helpers"; import { TopBarAuthentication } from "@/components/shared/Authentication/TopBarAuthentication"; import { CopyText } from "@/components/shared/CopyText/CopyText"; -import { EditorVersionToggle } from "@/components/shared/EditorVersionToggle"; import ImportPipeline from "@/components/shared/ImportPipeline"; -import { RunVersionToggle } from "@/components/shared/RunVersionToggle"; import { Button } from "@/components/ui/button"; import { Icon } from "@/components/ui/icon"; import { BlockStack, InlineStack } from "@/components/ui/layout"; @@ -56,7 +54,7 @@ const DefaultAppMenu = () => { const showAiModelQuickSelect = location.pathname.startsWith(APP_ROUTES.DASHBOARD_COMPONENTS) || location.pathname.startsWith(APP_ROUTES.DASHBOARD_COMPONENTS_V2) || - location.pathname.startsWith(APP_ROUTES.EDITOR_V2) || + location.pathname.startsWith(APP_ROUTES.EDITOR) || location.pathname.startsWith(APP_ROUTES.SETTINGS_AGENT); return ( @@ -126,9 +124,6 @@ const DefaultAppMenu = () => { {showAiModelQuickSelect && } - - - {/* Settings & status */} {isOnSettingsRoute ? ( { const AppMenu = () => { const { pathname } = useLocation(); - if (pathname.startsWith(APP_ROUTES.EDITOR_V2)) { + if (pathname.startsWith(APP_ROUTES.EDITOR)) { return null; } - if (pathname.startsWith(APP_ROUTES.RUNS_V2)) { + if (pathname.startsWith(`${APP_ROUTES.RUNS}/`)) { return null; } diff --git a/src/components/shared/ContextPanel/CollapsibleContextPanel.tsx b/src/components/shared/ContextPanel/CollapsibleContextPanel.tsx deleted file mode 100644 index aa080febc2..0000000000 --- a/src/components/shared/ContextPanel/CollapsibleContextPanel.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Icon } from "@/components/ui/icon"; -import { VerticalResizeHandle } from "@/components/ui/resize-handle"; -import { cn } from "@/lib/utils"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useContextPanel } from "@/providers/ContextPanelProvider"; - -import { ContextPanel } from "./ContextPanel"; - -const MIN_WIDTH = 200; -const MAX_WIDTH = 600; -const DEFAULT_WIDTH = 400; - -export function CollapsibleContextPanel() { - const { open, setOpen } = useContextPanel(); - - const { currentSubgraphPath } = useComponentSpec(); - - const isViewingSubgraph = currentSubgraphPath.length > 1; - - return ( - - - - - - - - {open && ( - - )} - - - - - - - ); -} diff --git a/src/components/shared/ContextPanel/ContextPanel.tsx b/src/components/shared/ContextPanel/ContextPanel.tsx deleted file mode 100644 index 066152e095..0000000000 --- a/src/components/shared/ContextPanel/ContextPanel.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useContextPanel } from "@/providers/ContextPanelProvider"; -import { BOTTOM_FOOTER_HEIGHT, TOP_NAV_HEIGHT } from "@/utils/constants"; - -export const ContextPanel = () => { - const { content } = useContextPanel(); - return ( - - {content} - - ); -}; diff --git a/src/components/shared/Dialogs/InputDialog.tsx b/src/components/shared/Dialogs/InputDialog.tsx deleted file mode 100644 index 7f9054003c..0000000000 --- a/src/components/shared/Dialogs/InputDialog.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { type ReactNode, useEffect, useState } from "react"; - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { Input } from "@/components/ui/input"; -import { Paragraph } from "@/components/ui/typography"; -import { cn } from "@/lib/utils"; - -type InputDialogProps = { - isOpen: boolean; - title: string; - description: string; - placeholder: string; - defaultValue: string; - content?: ReactNode; - disabled?: boolean; - validate?: (value: string) => string | null; - onConfirm?: (value: string) => void; - onCancel?: () => void; -}; - -export function InputDialog({ - isOpen, - title, - description, - placeholder, - defaultValue, - content, - disabled, - validate, - onConfirm, - onCancel, -}: InputDialogProps) { - const [value, setValue] = useState(defaultValue); - const [error, setError] = useState(null); - - const canConfirm = value.trim().length > 0 && !error && !disabled; - - const handleValueChange = (newValue: string) => { - setValue(newValue); - - if (validate) { - const validationError = validate(newValue.trim()); - setError(validationError); - } else { - setError(null); - } - }; - - const handleConfirm = () => { - const trimmedValue = value.trim(); - - if (trimmedValue.length === 0 || disabled) { - return; - } - - onConfirm?.(trimmedValue); - }; - - useEffect(() => { - if (isOpen) { - setValue(defaultValue); - setError(null); - } - }, [isOpen, defaultValue]); - - useEffect(() => { - if (isOpen && defaultValue && validate) { - const validationError = validate(defaultValue.trim()); - setError(validationError); - } - }, [isOpen, defaultValue, validate]); - - return ( - - - - {title} - {description} - - handleValueChange(e.target.value)} - placeholder={placeholder} - onEnter={handleConfirm} - onEscape={onCancel} - autoFocus - className={cn(!!error && "border-destructive")} - /> - {!!error && ( - - {error} - - )} - {content} - - Cancel - - Continue - - - - - ); -} diff --git a/src/components/shared/EditorVersionToggle.tsx b/src/components/shared/EditorVersionToggle.tsx deleted file mode 100644 index a9630b0224..0000000000 --- a/src/components/shared/EditorVersionToggle.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useLocation } from "@tanstack/react-router"; - -import { VersionToggle } from "@/components/shared/VersionToggle"; -import { - hasSeenEditorV2Welcome, - markEditorV2WelcomeSeen, -} from "@/components/shared/WelcomeSpotlight"; -import { APP_ROUTES, EDITOR_PATH } from "@/routes/router"; - -type EditorVersion = "v1" | "v2"; - -const detectEditorVersion = (pathname: string): EditorVersion | null => { - if (pathname.startsWith(`${APP_ROUTES.EDITOR_V2}/`)) return "v2"; - if (pathname.startsWith(`${EDITOR_PATH}/`)) return "v1"; - return null; -}; - -interface EditorVersionToggleProps { - showWelcomeSpotlight?: boolean; -} - -export const EditorVersionToggle = ({ - showWelcomeSpotlight = false, -}: EditorVersionToggleProps) => { - const location = useLocation(); - const version = detectEditorVersion(location.pathname); - if (!version) return null; - - const lastSegment = location.pathname.split("/").pop() ?? ""; - const pipelineName = decodeURIComponent(lastSegment); - if (!pipelineName) return null; - - const targetVersion = version === "v1" ? "v2" : "v1"; - const targetPath = - targetVersion === "v2" - ? `${APP_ROUTES.EDITOR_V2}/${encodeURIComponent(pipelineName)}` - : `${EDITOR_PATH}/${encodeURIComponent(pipelineName)}`; - const tooltip = - targetVersion === "v2" ? "Switch to new editor" : "Switch to legacy editor"; - - return ( - - ); -}; diff --git a/src/components/shared/PipelineDescription/PipelineDescription.tsx b/src/components/shared/PipelineDescription/PipelineDescription.tsx deleted file mode 100644 index 5b3b940949..0000000000 --- a/src/components/shared/PipelineDescription/PipelineDescription.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { ComponentSpec } from "@/utils/componentSpec"; - -import { ContentBlock } from "../ContextPanel/Blocks/ContentBlock"; -import { InlineEditor } from "../InlineEditor/InlineEditor"; -import { PipelineDescriptionEditor } from "./PipelineDescriptionEditor"; - -export const PipelineDescription = ({ - componentSpec, -}: { - componentSpec: ComponentSpec; -}) => { - const hasDescription = Boolean( - componentSpec.description && componentSpec.description.trim(), - ); - - if (!hasDescription) { - return ( - - - - ); - } - - return ( - - } - /> - - ); -}; diff --git a/src/components/shared/PipelineDescription/PipelineDescriptionEditor.tsx b/src/components/shared/PipelineDescription/PipelineDescriptionEditor.tsx deleted file mode 100644 index 66ad331812..0000000000 --- a/src/components/shared/PipelineDescription/PipelineDescriptionEditor.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; - -import { Textarea } from "@/components/ui/textarea"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { debounce } from "@/utils/debounce"; - -const TRACK_DEBOUNCE_MS = 100; - -interface PipelineDescriptionEditorProps { - autoFocus?: boolean; -} - -export const PipelineDescriptionEditor = ({ - autoFocus, -}: PipelineDescriptionEditorProps) => { - const { componentSpec, setComponentSpec } = useComponentSpec(); - const { track } = useAnalytics(); - - // eslint-disable-next-line no-restricted-syntax -- debounce() is stateful; compiler cannot prove purity - const debouncedTrack = useMemo( - () => - debounce(() => { - track("pipeline_editor.configuration_panel.text_field_edited", { - field: "description", - }); - }, TRACK_DEBOUNCE_MS), - [track], - ); - - useEffect(() => () => debouncedTrack.cancel(), [debouncedTrack]); - - const [localValue, setLocalValue] = useState(componentSpec.description ?? ""); - - const setDescription = (description: string) => { - debouncedTrack(); - setComponentSpec({ - ...componentSpec, - description: description || undefined, - }); - }; - - // Sync local value when componentSpec.description changes externally - useEffect(() => { - setLocalValue(componentSpec.description ?? ""); - }, [componentSpec.description]); - - return ( - setLocalValue(event.target.value)} - onBlur={() => setDescription(localValue)} - placeholder="Add a pipeline description..." - className="min-h-5 resize-y" - data-testid="pipeline-description-editor" - autoFocus={autoFocus} - /> - ); -}; diff --git a/src/components/shared/PipelineRunNameTemplate/PipelineRunNameTemplateEditor.tsx b/src/components/shared/PipelineRunNameTemplate/PipelineRunNameTemplateEditor.tsx deleted file mode 100644 index 3e9ee8f30b..0000000000 --- a/src/components/shared/PipelineRunNameTemplate/PipelineRunNameTemplateEditor.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useState } from "react"; - -import { Textarea } from "@/components/ui/textarea"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { - RUN_NAME_TEMPLATE_ANNOTATION, - setComponentSpecAnnotation, -} from "@/utils/annotations"; - -import { getRunNameTemplate } from "./utils"; - -interface PipelineRunNameTemplateEditorProps { - autoFocus?: boolean; -} - -export const PipelineRunNameTemplateEditor = ({ - autoFocus, -}: PipelineRunNameTemplateEditorProps) => { - const { componentSpec, setComponentSpec } = useComponentSpec(); - const [localValue, setLocalValue] = useState( - getRunNameTemplate(componentSpec), - ); - - const updateRunNameTemplate = (runNameTemplate: string | undefined) => { - if (runNameTemplate === undefined) { - return; - } - - setComponentSpec( - setComponentSpecAnnotation( - componentSpec, - RUN_NAME_TEMPLATE_ANNOTATION, - runNameTemplate, - ), - ); - }; - - return ( - setLocalValue(event.target.value)} - onBlur={() => updateRunNameTemplate(localValue)} - placeholder="Add a pipeline run name template..." - className="min-h-5 resize-y" - data-testid="pipeline-description-editor" - autoFocus={autoFocus} - /> - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/BulkUpdateConfirmationDialog.tsx b/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/BulkUpdateConfirmationDialog.tsx deleted file mode 100644 index 2374fc7a04..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/BulkUpdateConfirmationDialog.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import type { Node } from "@xyflow/react"; - -import type { InputSpec } from "@/utils/componentSpec"; - -import { thisCannotBeUndone } from "./shared"; - -export function getBulkUpdateConfirmationDetails( - nodesToUpdate: Node[], - unchangedNodes: Node[], - allLostInputs: InputSpec[], -) { - const hasLostInputs = allLostInputs.length > 0; - const hasExcludedNodes = unchangedNodes.length > 0; - - const nodeToUpdatePluralSuffix = nodesToUpdate.length === 1 ? "" : "s"; - - const title = `Upgrade Component${nodeToUpdatePluralSuffix}`; - const description = ""; - const content = ( - - - Are you sure you want to update{" "} - {nodesToUpdate.length === 1 ? "this node" : "these nodes"}? - - - This will upgrade {nodesToUpdate.length} node - {nodeToUpdatePluralSuffix} to {nodesToUpdate.length === 1 ? "a " : ""}{" "} - new version - {nodeToUpdatePluralSuffix} from{" "} - {nodesToUpdate.length === 1 ? "its" : "their"} source URL. - - {hasExcludedNodes && ( - <> - - - {unchangedNodes.length} custom component - {unchangedNodes.length === 1 ? "" : "s"} will not be updated. - - > - )} - {hasLostInputs && ( - <> - - Some input arguments may be lost. - > - )} - - {thisCannotBeUndone} - - ); - - return { - title, - content, - description, - }; -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/DeleteConfirmation.tsx b/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/DeleteConfirmation.tsx deleted file mode 100644 index e4a5d313cb..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/DeleteConfirmation.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { type Node } from "@xyflow/react"; - -import { BlockStack } from "@/components/ui/layout"; -import { Text } from "@/components/ui/typography"; -import { createStringList, truncate } from "@/utils/string"; - -import { getFlexNodeDisplayName } from "../FlexNode/utils"; -import { isFlexNode, type NodesAndEdges } from "../types"; -import { thisCannotBeUndone } from "./shared"; - -export function getDeleteConfirmationDetails(deletedElements: NodesAndEdges) { - const deletedNodes = deletedElements.nodes; - const deletedEdges = deletedElements.edges; - - if (deletedNodes.length > 0) { - const isDeletingMultipleNodes = deletedNodes.length > 1; - - if (!isDeletingMultipleNodes) { - const node = deletedNodes[0]; - - if (isFlexNode(node)) { - const singleDeleteTitle = `Delete Sticky Note?`; - const singleDeleteDesc = node.data.properties.title - ? `Title: '${node.data.properties.title}'` - : node.data.properties.content - ? `Content: '${truncate(node.data.properties.content, 12, { breakWords: false })}'` - : "This sticky note has no text content"; - - return { - title: singleDeleteTitle, - description: singleDeleteDesc, - }; - } - - const singleDeleteTitle = - "Delete Node" + - (deletedNodes.length > 0 ? ` '${deletedNodes[0].id}'` : "") + - "?"; - - const singleDeleteDesc = ( - - - This will also delete all connections to and from the Node. - - {thisCannotBeUndone} - - ); - - return { - title: singleDeleteTitle, - content: singleDeleteDesc, - description: "", - }; - } - - const sortedDeletedNodes = sortFlexNodesLast(deletedNodes); - - const multiDeleteTitle = `Delete Nodes?`; - - const deletedNodeList = createStringList( - getNodeIdsForDisplay(sortedDeletedNodes), - 2, - "node", - ); - - if (sortedDeletedNodes.every(isFlexNode)) { - const multiDeleteDesc = ( - - {`This will delete ${deletedNodeList}.`} - - {thisCannotBeUndone} - - ); - - return { - title: multiDeleteTitle, - content: multiDeleteDesc, - description: "", - }; - } - - const multiDeleteDesc = ( - - {`Deleting ${deletedNodeList} will also remove all connections to and from these nodes.`} - - {thisCannotBeUndone} - - ); - - return { - title: multiDeleteTitle, - content: multiDeleteDesc, - description: "", - }; - } - - if (deletedEdges.length > 0) { - const isDeletingMultipleEdges = deletedEdges.length > 1; - - const edgeDeleteTitle = isDeletingMultipleEdges - ? "Delete Connections?" - : "Delete Connection?"; - - const edgeDeleteDesc = ( - - - This will remove the follow connections between task nodes: - - - {deletedEdges - .map((edge) => { - return `'${edge.id}'`; - }) - .join(", ")} - - - {thisCannotBeUndone} - - ); - - return { - title: edgeDeleteTitle, - content: edgeDeleteDesc, - description: "", - }; - } - - // Fallback to default - return {}; -} - -function getNodeIdsForDisplay(nodes: Node[]) { - return nodes.map((node) => { - if (isFlexNode(node)) { - const textContent = getFlexNodeDisplayName(node.data); - return `'${textContent}' (Sticky Note)`; - } - - return node.id; - }); -} - -function sortFlexNodesLast(nodes: Node[]) { - return [...nodes].sort((a, b) => { - const aIsFlex = isFlexNode(a); - const bIsFlex = isFlexNode(b); - - if (aIsFlex && !bIsFlex) return 1; - if (!aIsFlex && bIsFlex) return -1; - - return 0; - }); -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/ReplaceConfirmation.tsx b/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/ReplaceConfirmation.tsx deleted file mode 100644 index ed9bf8b791..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/ReplaceConfirmation.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { type Node } from "@xyflow/react"; -import { Unlink } from "lucide-react"; - -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import type { InputSpec, TaskSpec } from "@/utils/componentSpec"; - -import { getArgumentDetails, thisCannotBeUndone } from "./shared"; - -export function getReplaceConfirmationDetails( - replacedNode: Node, - newTaskId: string, - lostInputs: InputSpec[], -) { - const oldTaskId = replacedNode.data.taskId as string; - const oldTaskSpec = replacedNode.data.taskSpec as TaskSpec | undefined; - const oldTaskInputs = oldTaskSpec?.componentRef.spec?.inputs; - const oldTaskArguments = oldTaskSpec?.arguments; - - const hasLostInputs = lostInputs.length > 0; - const hasMigratedInputs = - oldTaskInputs && lostInputs.length < oldTaskInputs.length; - - const argumentsList = ( - - {lostInputs.map((input) => { - const argument = getArgumentDetails(oldTaskArguments, input.name); - - return ( - - - - - - - Linked node will be disconnected - - - - {input.name} - {`(${input.type})`} - : - - {argument.value} - - - - ); - })} - - ); - - const title = "Replace Node?"; - const description = ""; - const content = ( - - {`Are you sure you want to replace "${oldTaskId}" with "${newTaskId}"?`} - - {hasLostInputs ? ( - <> - The following input arguments will be removed: - {argumentsList} - {hasMigratedInputs && ( - <> - - All other inputs will be preserved. - > - )} - > - ) : ( - All inputs will be preserved. - )} - - {thisCannotBeUndone} - - ); - - return { - title, - description, - content, - }; -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/UpgradeComponent.tsx b/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/UpgradeComponent.tsx deleted file mode 100644 index 02ae622fbe..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/UpgradeComponent.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Unlink } from "lucide-react"; - -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import type { InputSpec, TaskSpec } from "@/utils/componentSpec"; - -import { getArgumentDetails, thisCannotBeUndone } from "./shared"; - -export function getUpgradeConfirmationDetails( - taskId: string, - taskSpec: TaskSpec | undefined, - newComponentDigest: string, - lostInputs: InputSpec[], -) { - const oldTaskId = taskId; - const oldTaskSpec = taskSpec; - const oldTaskInputs = oldTaskSpec?.componentRef.spec?.inputs; - const oldTaskArguments = oldTaskSpec?.arguments; - const oldTaskDigest = oldTaskSpec?.componentRef.digest; - - const hasLostInputs = lostInputs.length > 0; - const hasMigratedInputs = - oldTaskInputs && lostInputs.length < oldTaskInputs.length; - - const argumentsList = ( - - {lostInputs.map((input) => { - const argument = getArgumentDetails(oldTaskArguments, input.name); - - return ( - - - - - - - Linked node will be disconnected - - - {input.name} - {`(${input.type})`} - : - {argument.value} - - ); - })} - - ); - - const title = `Update ${oldTaskId}?`; - const description = ""; - const content = ( - - - - This will upgrade the Task from version{" "} - - - - {oldTaskDigest?.substring(0, 8)} - - - {oldTaskDigest} - {" "} - to version{" "} - - - - {newComponentDigest?.substring(0, 8)} - - - - {newComponentDigest} - - - - - - {hasLostInputs ? ( - <> - The following input arguments will be removed: - {argumentsList} - {hasMigratedInputs && ( - <> - - All other inputs will be preserved. - > - )} - > - ) : ( - All inputs will be preserved. - )} - - {thisCannotBeUndone} - - ); - - return { - title, - description, - content, - }; -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/shared.tsx b/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/shared.tsx deleted file mode 100644 index c1f54c04e0..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/shared.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Paragraph } from "@/components/ui/typography"; -import type { ArgumentType } from "@/utils/componentSpec"; - -export const thisCannotBeUndone = ( - This cannot be undone. -); - -export function getArgumentDetails( - taskArguments: { [k: string]: ArgumentType } | undefined, - inputName: string, -) { - const notSet = "No value"; - - if (!taskArguments) { - return { value: notSet, isBrokenConnection: false }; - } - - const argument = taskArguments[inputName]; - - if (!argument) { - return { value: notSet, isBrokenConnection: false }; - } - - if (typeof argument === "object" && argument !== null) { - if ("taskOutput" in argument && argument.taskOutput) { - return { - value: `from "${argument.taskOutput.taskId}"`, - isBrokenConnection: true, - }; - } - if ("graphInput" in argument && argument.graphInput) { - return { - value: `"${argument.graphInput.inputName}"`, - isBrokenConnection: true, - }; - } - } - - if (typeof argument === "string") { - if (argument === "") { - return { value: notSet, isBrokenConnection: false }; - } - return { value: `"${argument}"`, isBrokenConnection: false }; - } - - return { value: notSet, isBrokenConnection: false }; -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/Edges/ConnectionLine.tsx b/src/components/shared/ReactFlow/FlowCanvas/Edges/ConnectionLine.tsx deleted file mode 100644 index 483f8ac604..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Edges/ConnectionLine.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - type ConnectionLineComponent, - type ConnectionLineComponentProps, - getBezierPath, - type Node, - useConnection, - useNodes, -} from "@xyflow/react"; - -import { EdgeColor } from "./utils"; - -export const ConnectionLine: ConnectionLineComponent = ({ - fromX, - fromY, - fromPosition, - toX, - toY, - toPosition, - connectionStatus, -}: ConnectionLineComponentProps) => { - const nodes = useNodes(); - const sourceNode = useConnection((connection) => connection.fromNode); - const targetNode = useConnection((connection) => connection.toNode); - - const hasGhostNode = nodes.some((node) => node.type === "ghost"); - - if (hasGhostNode) { - return null; - } - - const [path] = getBezierPath({ - sourceX: fromX, - sourceY: fromY, - sourcePosition: fromPosition, - targetX: toX, - targetY: toY, - targetPosition: toPosition, - }); - - let color = - connectionStatus === "valid" ? EdgeColor.Valid : EdgeColor.Incomplete; - - if (sourceNode && targetNode && !isValidConnection(sourceNode, targetNode)) { - color = EdgeColor.Invalid; - } - - const markerId = `connection-line-arrow-${color}`; - - return ( - - - - - - - - - - - ); -}; - -const isValidConnection = (fromNode: Node, toNode: Node) => { - if (fromNode.id === toNode.id) { - return false; - } - - // IO nodes can't connect to each other - const isFromIO = fromNode.type === "input" || fromNode.type === "output"; - const isToIO = toNode.type === "input" || toNode.type === "output"; - - if (isFromIO && isToIO) { - return false; - } - - return true; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Edges/SmoothEdge.tsx b/src/components/shared/ReactFlow/FlowCanvas/Edges/SmoothEdge.tsx deleted file mode 100644 index e771cb702f..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Edges/SmoothEdge.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import type { EdgeProps } from "@xyflow/react"; -import { getBezierPath, useEdges } from "@xyflow/react"; - -import { EdgeColor } from "./utils"; - -const SmoothEdge = ({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - style = {}, - selected, -}: EdgeProps) => { - const edges = useEdges(); - const hasAnySelectedEdge = edges.some((edge) => edge.selected); - - const [edgePath] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - const getEdgeColor = () => { - if (selected) return EdgeColor.Selected; - if (hasAnySelectedEdge) return EdgeColor.Muted; - return EdgeColor.Neutral; - }; - - const edgeColor = getEdgeColor(); - const markerIdSuffix = selected - ? "selected" - : hasAnySelectedEdge - ? "muted" - : "default"; - - return ( - <> - - - - - - - - - - - {/* Invisible hitbox path */} - - {/* Visible edge path */} - - > - ); -}; - -export default SmoothEdge; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Edges/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/Edges/utils.ts index 298651317e..f6ae94f5e0 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/Edges/utils.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/Edges/utils.ts @@ -1,11 +1 @@ -// Note: Selected color is also defined as --edge-selected CSS variable in global.css -export enum EdgeColor { - Incomplete = "#b1b1b7", - Neutral = "#6b7280", - Muted = "#d1d5db", - Valid = "#8db88d", - Invalid = "#d47a7a", - Input = "#3b82f6", - Output = "#a855f7", - Selected = "#5b2ef4", -} +export const EDGE_SELECTED_COLOR = "#5b2ef4"; diff --git a/src/components/shared/ReactFlow/FlowCanvas/FlexNode/FlexNode.tsx b/src/components/shared/ReactFlow/FlowCanvas/FlexNode/FlexNode.tsx deleted file mode 100644 index 9bf8670e91..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/FlexNode/FlexNode.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { - type Node, - type NodeProps, - NodeResizer, - type ResizeDragEvent, - type ResizeParams, -} from "@xyflow/react"; -import { type MouseEvent, useEffect, useState } from "react"; - -import { BlockStack } from "@/components/ui/layout"; -import { useIsMultiSelect } from "@/hooks/useIsMultiSelect"; -import { cn } from "@/lib/utils"; -import { useContextPanel } from "@/providers/ContextPanelProvider"; - -import { FlexNodeEditor } from "./FlexNodeEditor"; -import { InlineTextEditor } from "./InlineTextEditor"; -import LockToggle from "./LockToggle"; -import type { FlexNodeData } from "./types"; -import { useFlexNodeUpdate } from "./useFlexNodeUpdate"; - -type FlexNodeProps = NodeProps>; - -const MIN_SIZE = { width: 50, height: 50 }; - -const FlexNode = ({ data, id, selected }: FlexNodeProps) => { - const { properties, readOnly, locked = false } = data; - const { - title, - content, - color, - borderColor, - titleFontSize = 12, - contentFontSize = 10, - } = properties; - - const [isContextPanelFocus, setIsContextPanelFocus] = useState(false); - const [isInlineEditingContent, setIsInlineEditingContent] = useState(false); - const [isInlineEditingTitle, setIsInlineEditingTitle] = useState(false); - - const { - setContent, - clearContent, - setOpen: setContextPanelOpen, - } = useContextPanel(); - - const { updateFlexNode, updateProperties } = useFlexNodeUpdate(data); - - const { isMultiSelect, isMultiSelectRef } = useIsMultiSelect(); - - const toggleLock = () => { - updateFlexNode({ locked: !locked }); - }; - - const handleClick = (e: MouseEvent) => { - if (locked) { - e.stopPropagation(); - setIsContextPanelFocus(true); - setContent( - , - ); - setContextPanelOpen(true); - } - }; - - const handleDoubleClick = (e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - - if (locked) { - return; - } - - if (!readOnly) { - setIsInlineEditingTitle(false); - setIsInlineEditingContent(true); - } - }; - - const handleDoubleClickTitle = (e: MouseEvent) => { - e.stopPropagation(); - if (locked) { - toggleLock(); - return; - } - if (!readOnly) { - setIsInlineEditingContent(false); - setIsInlineEditingTitle(true); - } - }; - - const handleResizeEnd = (_: ResizeDragEvent, params: ResizeParams) => { - const width = Math.max(params.width, MIN_SIZE.width); - const height = Math.max(params.height, MIN_SIZE.height); - - updateFlexNode({ - size: { width, height }, - position: { x: params.x, y: params.y }, - }); - }; - - const handleSaveContent = (newContent: string) => { - updateProperties({ - content: newContent, - }); - setIsInlineEditingContent(false); - }; - - const handleSaveTitle = (newTitle: string) => { - updateProperties({ - title: newTitle, - }); - setIsInlineEditingTitle(false); - }; - - const switchEditor = () => { - setIsInlineEditingTitle((prev) => !prev); - setIsInlineEditingContent((prev) => !prev); - }; - - useEffect(() => { - if (selected && !isMultiSelect) { - setIsContextPanelFocus(true); - setContent( - , - ); - setContextPanelOpen(true); - } else { - setIsContextPanelFocus(false); - } - - return () => { - if (selected && !isMultiSelectRef.current) { - clearContent(); - } - }; - }, [selected, isMultiSelect]); - - useEffect(() => { - if (isContextPanelFocus) { - setContent( - , - ); - } - }, [data, locked, isContextPanelFocus, id, readOnly]); - - const isTransparent = color === "transparent"; - const isBorderTransparent = borderColor === "transparent"; - - // Filled notes keep dark ink so text never inverts against the colour fill; - // transparent notes sit on the canvas and follow the theme foreground. - const inkClass = isTransparent ? "text-foreground" : "text-ink-fixed"; - - return ( - <> - {!readOnly && ( - - )} - - - - - - {title && - (isInlineEditingTitle ? ( - setIsInlineEditingTitle(false)} - onTab={switchEditor} - className={cn("font-bold", inkClass)} - /> - ) : ( - - {title} - - ))} - - {isInlineEditingContent ? ( - setIsInlineEditingContent(false)} - onTab={switchEditor} - className={inkClass} - /> - ) : ( - - {content} - - )} - - - - > - ); -}; - -export default FlexNode; diff --git a/src/components/shared/ReactFlow/FlowCanvas/FlexNode/FlexNodeEditor.tsx b/src/components/shared/ReactFlow/FlowCanvas/FlexNode/FlexNodeEditor.tsx deleted file mode 100644 index 6e332f6db4..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/FlexNode/FlexNodeEditor.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import { type ChangeEvent, useEffect, useState } from "react"; - -import { LinkNodeButton } from "@/components/shared/Buttons/LinkNodeButton"; -import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock"; -import { KeyValueList } from "@/components/shared/ContextPanel/Blocks/KeyValueList"; -import { CopyText } from "@/components/shared/CopyText/CopyText"; -import { InfoBox } from "@/components/shared/InfoBox"; -import { ColorPicker } from "@/components/ui/color"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Textarea } from "@/components/ui/textarea"; -import { Paragraph, Text } from "@/components/ui/typography"; -import { cn } from "@/lib/utils"; -import { FONT_SIZE_MD, FONT_SIZE_SM } from "@/utils/constants"; - -import { StackingControls } from "../../FlowControls/StackingControls"; -import LockToggle from "./LockToggle"; -import { TextSizeSelector } from "./TextSizeSelector"; -import type { FlexNodeData } from "./types"; -import { useFlexNodeUpdate } from "./useFlexNodeUpdate"; -import { DEFAULT_BORDER_COLOR } from "./utils"; - -interface FlexNodeEditorProps { - flexNode: FlexNodeData; - readOnly?: boolean; - toggleLock?: () => void; -} - -export const FlexNodeEditor = ({ - flexNode, - readOnly = false, - toggleLock, -}: FlexNodeEditorProps) => { - const { metadata, zIndex, size, position, locked = false } = flexNode; - - return ( - - - - Sticky Note - - {toggleLock && ( - - )} - - - - - - - - - - - {!readOnly && !locked && } - - {readOnly && } - - ); -}; - -const ContentEditor = ({ - flexNode, - readOnly, -}: { - flexNode: FlexNodeData; - readOnly: boolean; -}) => { - const { updateProperties } = useFlexNodeUpdate(flexNode); - - const { properties } = flexNode; - - const [title, setTitle] = useState(properties.title); - const [content, setContent] = useState(properties.content); - - const handleTitleChange = (e: ChangeEvent) => { - setTitle(e.target.value); - }; - - const handleContentChange = (e: ChangeEvent) => { - setContent(e.target.value); - }; - - const handleTitleFontSizeChange = (newSize: number) => { - updateProperties({ titleFontSize: newSize }); - }; - - const handleContentFontSizeChange = (newSize: number) => { - updateProperties({ contentFontSize: newSize }); - }; - - const saveChanges = () => { - updateProperties({ title, content }); - }; - - useEffect(() => { - setTitle(properties.title); - setContent(properties.content); - }, [properties]); - - if (readOnly) { - return ( - - ); - } - - return ( - - - - - - Title - - - - - - - - - Note - - - - - - - - ); -}; - -const ColorEditor = ({ - flexNode, - readOnly, -}: { - flexNode: FlexNodeData; - readOnly: boolean; -}) => { - const { updateProperties } = useFlexNodeUpdate(flexNode); - - const { properties } = flexNode; - - const currentBorderColor = properties.borderColor ?? DEFAULT_BORDER_COLOR; - - const [backgroundColor, setBackgroundColor] = useState(properties.color); - const [borderColor, setBorderColor] = useState(currentBorderColor); - - const isTransparent = backgroundColor === "transparent"; - const isBorderTransparent = borderColor === "transparent"; - const hasTextContent = properties.title || properties.content; - - const handleBackgroundColorChange = (newColor: string) => { - setBackgroundColor(newColor); - saveColors(newColor, borderColor); - }; - - const handleBorderColorChange = (newColor: string) => { - setBorderColor(newColor); - saveColors(backgroundColor, newColor); - }; - - const saveColors = ( - newBackgroundColor: string, - newBorderColor: string | undefined, - ) => { - updateProperties({ - color: newBackgroundColor, - borderColor: newBorderColor, - }); - }; - - useEffect(() => { - setBackgroundColor(properties.color); - setBorderColor(currentBorderColor); - }, [properties]); - - if (readOnly) { - return ( - - ); - } - - return ( - - - - Background - - {properties.color} - - - {isTransparent && ( - - Border - - - {currentBorderColor} - - - )} - - {isTransparent && isBorderTransparent && !hasTextContent && ( - - This sticky note has no visual context. Consider adding a border, - background or text. - - )} - - - ); -}; - -const ZIndexEditor = ({ flexNode }: { flexNode: FlexNodeData }) => { - const { updateFlexNode } = useFlexNodeUpdate(flexNode); - - const handleStackingControlChange = (newZIndex: number) => { - updateFlexNode({ zIndex: newZIndex }); - }; - - return ( - - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/FlexNode/useFlexNodeUpdate.ts b/src/components/shared/ReactFlow/FlowCanvas/FlexNode/useFlexNodeUpdate.ts deleted file mode 100644 index ba37fd74ce..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/FlexNode/useFlexNodeUpdate.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { updateFlexNodeInComponentSpec } from "@/components/shared/ReactFlow/FlowCanvas/FlexNode/interface"; -import type { FlexNodeData } from "@/components/shared/ReactFlow/FlowCanvas/FlexNode/types"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { updateSubgraphSpec } from "@/utils/subgraphUtils"; - -export const useFlexNodeUpdate = (flexNode: FlexNodeData) => { - const { - componentSpec, - currentSubgraphSpec, - currentSubgraphPath, - setComponentSpec, - } = useComponentSpec(); - - const updateFlexNode = (updates: Partial) => { - const updatedFlexNode = { - ...flexNode, - ...updates, - }; - - const updatedSubgraphSpec = updateFlexNodeInComponentSpec( - currentSubgraphSpec, - updatedFlexNode, - ); - - const newRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(newRootSpec); - }; - - const updateProperties = ( - propertyUpdates: Partial, - ) => { - updateFlexNode({ - properties: { - ...flexNode.properties, - ...propertyUpdates, - }, - }); - }; - - return { updateFlexNode, updateProperties }; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx b/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx deleted file mode 100644 index 9dfa058132..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx +++ /dev/null @@ -1,1301 +0,0 @@ -import { - type Connection, - type Edge, - type EdgeChange, - type FinalConnectionState, - type Node, - type NodeChange, - NodeToolbar, - type OnInit, - ReactFlow, - type ReactFlowInstance, - type ReactFlowProps, - SelectionMode, - useConnection, - useEdgesState, - useNodesState, - useStoreApi, - useUpdateNodeInternals, - type XYPosition, -} from "@xyflow/react"; -import type { DragEvent, Ref } from "react"; -import { useEffect, useImperativeHandle, useRef, useState } from "react"; - -import { ConfirmationDialog } from "@/components/shared/Dialogs"; -import { BlockStack } from "@/components/ui/layout"; -import useComponentSpecToEdges from "@/hooks/useComponentSpecToEdges"; -import useConfirmationDialog from "@/hooks/useConfirmationDialog"; -import { useCopyPaste } from "@/hooks/useCopyPaste"; -import { useGhostNode } from "@/hooks/useGhostNode"; -import { useIOSelectionPersistence } from "@/hooks/useIOSelectionPersistence"; -import { useNodeCallbacks } from "@/hooks/useNodeCallbacks"; -import { addRecentlyUsed } from "@/hooks/useRecentlyViewed"; -import { useSubgraphKeyboardNavigation } from "@/hooks/useSubgraphKeyboardNavigation"; -import useToastNotification from "@/hooks/useToastNotification"; -import { useUserDetails } from "@/hooks/useUserDetails"; -import { cn } from "@/lib/utils"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { useComponentLibrary } from "@/providers/ComponentLibraryProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useContextPanel } from "@/providers/ContextPanelProvider"; -import { hydrateComponentReference } from "@/services/componentService"; -import { - type ComponentSpec, - type InputSpec, - isNotMaterializedComponentReference, - type TaskSpec, -} from "@/utils/componentSpec"; -import { componentMetadata } from "@/utils/componentTracking"; -import { readTextFromFile } from "@/utils/dom"; -import { deselectAllNodes } from "@/utils/flowUtils"; -import createNodesFromComponentSpec from "@/utils/nodes/createNodesFromComponentSpec"; -import { - getSubgraphComponentSpec, - updateSubgraphSpec, -} from "@/utils/subgraphUtils"; - -import { SuspenseWrapper } from "../../SuspenseWrapper"; -import { useNodesOverlay } from "../NodesOverlay/NodesOverlayProvider"; -import { getBulkUpdateConfirmationDetails } from "./ConfirmationDialogs/BulkUpdateConfirmationDialog"; -import { getDeleteConfirmationDetails } from "./ConfirmationDialogs/DeleteConfirmation"; -import { getReplaceConfirmationDetails } from "./ConfirmationDialogs/ReplaceConfirmation"; -import { ConnectionLine } from "./Edges/ConnectionLine"; -import type { GhostNodeData } from "./GhostNode/types"; -import { - computeDropPositionFromRefs, - createGhostEdge, -} from "./GhostNode/utils"; -import { MultiSelectPanel } from "./Multiselect/MultiSelectPanel"; -import SelectionToolbar from "./Multiselect/SelectionToolbar"; -import { handleGroupNodes } from "./Subgraphs/create/handleGroupNodes"; -import { NewSubgraphDialog } from "./Subgraphs/create/NewSubgraphDialog"; -import { canGroupNodes } from "./Subgraphs/create/utils"; -import { SubgraphBreadcrumbs } from "./Subgraphs/view/SubgraphBreadcrumbs"; -import { - edgeTypes, - isTaskNodeType, - type NodesAndEdges, - nodeTypes, -} from "./types"; -import addFlexNode from "./utils/addFlexNode"; -import addTask from "./utils/addTask"; -import { autoLayoutNodes, type LayoutAlgorithm } from "./utils/autolayout"; -import { createConnectedIONode } from "./utils/createConnectedIONode"; -import { duplicateNodes } from "./utils/duplicateNodes"; -import { isPositionInNode } from "./utils/geometry"; -import { getNodeFromEvent } from "./utils/getNodeFromEvent"; -import { getPositionFromEvent } from "./utils/getPositionFromEvent"; -import { handleAggregatorConnection } from "./utils/handleAggregatorConnection"; -import { handleAggregatorEdgeDeletion } from "./utils/handleAggregatorEdgeDeletion"; -import { handleConnection } from "./utils/handleConnection"; -import { removeEdge } from "./utils/removeEdge"; -import { removeNode } from "./utils/removeNode"; -import { replaceTaskNode } from "./utils/replaceTaskNode"; -import { updateNodePositions } from "./utils/updateNodePosition"; - -const SELECTABLE_NODES = new Set(["task", "input", "output", "flex"]); -const UPGRADEABLE_NODES = new Set(["task"]); -const REPLACEABLE_NODES = new Set(["task"]); -const FAST_PLACE_NODE_TYPES = new Set(["task"]); - -const useScheduleExecutionOnceWhenConditionMet = ( - condition: boolean, - callback: () => void, -) => { - const hasExecuted = useRef(false); - useEffect(() => { - if (condition && !hasExecuted.current) { - callback(); - hasExecuted.current = true; - } - }, [condition, callback]); -}; - -export interface FlowCanvasRef { - autoLayout: (algorithm: LayoutAlgorithm) => Promise; -} - -interface FlowCanvasProps extends ReactFlowProps { - readOnly?: boolean; - fitViewOnInit?: boolean; - ref?: Ref; -} - -const FlowCanvas = ({ ref, ...props }: FlowCanvasProps) => { - return ( - - - - ); -}; - -const FlowCanvasContent = ({ - readOnly, - nodesConnectable, - fitViewOnInit = true, - ref, - children, - ...rest -}: FlowCanvasProps) => { - const initialCanvasLoaded = useRef(false); - const initialFitDone = useRef(false); - - const { clearContent, setContent, setOpen } = useContextPanel(); - const { data: currentUserDetails } = useUserDetails(); - - useSubgraphKeyboardNavigation(); - const { setReactFlowInstance: setReactFlowInstanceForOverlay } = - useNodesOverlay(); - const { - componentSpec, - setComponentSpec, - currentGraphSpec, - currentSubgraphSpec, - updateGraphSpec, - currentSubgraphPath, - } = useComponentSpec(); - const { preserveIOSelectionOnSpecChange, resetPrevSpec } = - useIOSelectionPersistence(); - - const updateNodeInternals = useUpdateNodeInternals(); - - const store = useStoreApi(); - const getSelectedNodes = () => - store - .getState() - .nodes.filter( - (node) => node.selected && node.type && SELECTABLE_NODES.has(node.type), - ); - - const { edges: specEdges, onEdgesChange } = - useComponentSpecToEdges(currentSubgraphSpec); - const [nodes, setNodes, onNodesChange] = useNodesState([]); - const [edges, setEdges] = useEdgesState(specEdges); - - const isBoxSelecting = useRef(false); - - const handleEdgesChange = (changes: EdgeChange[]) => { - if (!isBoxSelecting.current) { - onEdgesChange(changes); - return; - } - - const filtered = changes.filter((change) => change.type !== "select"); - if (filtered.length > 0) { - onEdgesChange(filtered); - } - }; - - const isConnecting = useConnection((connection) => connection.inProgress); - const connectionSourceHandle = useConnection( - (connection) => connection.fromHandle, - ); - - const { - handlers: confirmationHandlers, - triggerDialog: triggerConfirmation, - ...confirmationProps - } = useConfirmationDialog(); - - const [showNewSubgraphDialog, setShowNewSubgraphDialog] = useState(false); - - const notify = useToastNotification(); - - const latestFlowPosRef = useRef(null); - const ghostNodeRef = useRef | null>(null); - const shouldCreateIONodeRef = useRef(false); - - const [showToolbar, setShowToolbar] = useState(false); - const [replaceTarget, setReplaceTarget] = useState(null); - const [shiftKeyPressed, setShiftKeyPressed] = useState(false); - const [metaKeyPressed, setMetaKeyPressed] = useState(false); - - const { addToComponentLibrary } = useComponentLibrary(); - const { track } = useAnalytics(); - - const handleKeyDown = (event: KeyboardEvent) => { - const target = event.target as HTMLElement; - const isInputFocused = - target.tagName === "INPUT" || - target.tagName === "TEXTAREA" || - target.isContentEditable || - target.closest('[data-slot="input"]'); - - if (isInputFocused) { - return; - } - - if (event.key === "Shift") { - setShiftKeyPressed(true); - } - - if (event.key === "Meta" || event.key === "Control") { - setMetaKeyPressed(true); - } - - if (event.key === "a" && (event.metaKey || event.ctrlKey)) { - event.preventDefault(); - - setNodes((currentNodes) => - currentNodes.map((node) => ({ - ...node, - selected: - event.shiftKey || !node.type - ? false - : SELECTABLE_NODES.has(node.type), - })), - ); - } - }; - - const handleKeyUp = (event: KeyboardEvent) => { - if (event.key === "Shift") { - setShiftKeyPressed(false); - } - if (event.key === "Meta" || event.key === "Control") { - setMetaKeyPressed(false); - } - }; - - const handleMouseDown = (event: MouseEvent) => { - if (event.shiftKey && event.target instanceof HTMLElement) { - const reactFlowWrapper = event.target.closest(".react-flow"); - if (reactFlowWrapper) { - event.preventDefault(); - } - } - }; - - useEffect(() => { - document.addEventListener("keydown", handleKeyDown); - document.addEventListener("keyup", handleKeyUp); - document.addEventListener("mousedown", handleMouseDown); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("keyup", handleKeyUp); - document.removeEventListener("mousedown", handleMouseDown); - }; - }, [handleKeyDown, handleKeyUp, handleMouseDown]); - - const [reactFlowInstance, setReactFlowInstance] = - useState(); - - const onInit: OnInit = (instance) => { - setReactFlowInstance(instance); - setReactFlowInstanceForOverlay(instance); - }; - - const updateOrAddNodes = ({ - updatedNodes, - newNodes, - }: { - updatedNodes?: Node[]; - newNodes?: Node[]; - }) => { - setNodes((prev) => { - const updated = prev.map((node) => { - const updatedNode = updatedNodes?.find( - (updatedNode) => updatedNode.id === node.id, - ); - return updatedNode ? { ...node, ...updatedNode } : node; - }); - - if (!newNodes) { - return updated; - } - - return [...updated, ...newNodes]; - }); - }; - - const { ghostNode, shouldCreateIONode } = useGhostNode({ - readOnly, - active: metaKeyPressed, - isConnecting, - implementation: currentSubgraphSpec.implementation, - }); - - useEffect(() => { - if (!!ghostNode && connectionSourceHandle) { - const ghostEdge = createGhostEdge(connectionSourceHandle); - setEdges([...specEdges, ghostEdge]); - - return; - } - - setEdges(specEdges); - }, [ghostNode, connectionSourceHandle, specEdges, setEdges]); - - const nodesForRender: Node[] = ghostNode ? [...nodes, ghostNode] : nodes; - - useEffect(() => { - shouldCreateIONodeRef.current = shouldCreateIONode; - }, [shouldCreateIONode]); - - useEffect(() => { - ghostNodeRef.current = ghostNode; - }, [ghostNode]); - - const selectedNodes = nodes.filter( - (node) => node.selected && node.type && SELECTABLE_NODES.has(node.type), - ); - const selectedNodeIds = selectedNodes.map((node) => node.id).join(","); - - const canUpgrade = selectedNodes.some( - (node) => node.type && UPGRADEABLE_NODES.has(node.type), - ); - - const { canGroup } = canGroupNodes(selectedNodes); - - const onElementsRemove = (params: NodesAndEdges) => { - let updatedSubgraphSpec = { ...currentSubgraphSpec }; - - // Collect all edges that need to be removed, including those connected to deleted nodes - const edgesToRemove = [...params.edges]; - - for (const node of params.nodes) { - const connectedEdges = edges.filter( - (edge) => edge.source === node.id || edge.target === node.id, - ); - for (const edge of connectedEdges) { - if (!edgesToRemove.find((e) => e.id === edge.id)) { - edgesToRemove.push(edge); - } - } - } - - // Handle aggregator input cleanup for deleted edges - updatedSubgraphSpec = handleAggregatorEdgeDeletion( - updatedSubgraphSpec, - edgesToRemove, - ); - - for (const edge of edgesToRemove) { - updatedSubgraphSpec = removeEdge(edge, updatedSubgraphSpec); - } - - for (const node of params.nodes) { - updatedSubgraphSpec = removeNode(node, updatedSubgraphSpec); - } - - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(updatedRootSpec); - }; - - const nodeCallbacks = useNodeCallbacks({ - triggerConfirmation, - onElementsRemove, - updateOrAddNodes, - }); - - const nodeData = { - connectable: !readOnly && !!nodesConnectable, - readOnly, - nodeCallbacks, - }; - - const updateReactFlow = (newComponentSpec: ComponentSpec) => { - const subgraphSpec = getSubgraphComponentSpec( - newComponentSpec, - currentSubgraphPath, - notify, - ); - const newNodes = createNodesFromComponentSpec( - subgraphSpec, - nodeData, - readOnly, - ); - - const updatedNewNodes = newNodes.map((node) => ({ - ...node, - data: { - ...node.data, - highlighted: node.id === replaceTarget?.id, - }, - })); - - setNodes((prevNodes) => { - const updatedNodes = updatedNewNodes.map((newNode) => { - const existingNode = prevNodes.find((node) => node.id === newNode.id); - if (!existingNode) { - return newNode; - } - - // For task nodes, completely replace the data to ensure React sees the change - if (newNode.type === "task") { - return { - ...existingNode, - ...newNode, - data: newNode.data, // Complete replacement for task nodes - }; - } - - return { - ...existingNode, - ...newNode, - data: { - ...existingNode.data, - ...newNode.data, - }, - }; - }); - - return updatedNodes; - }); - }; - - // Track the most recent graphSpec to avoid stale state issues with quick consecutive connections - const latestGraphSpecRef = useRef(currentGraphSpec); - - useEffect(() => { - latestGraphSpecRef.current = currentGraphSpec; - }, [currentGraphSpec]); - - const onConnect = (connection: Connection) => { - if (connection.source === connection.target) return; - - const isAggregatorConnection = - connection.targetHandle === "__add_aggregator_input__"; - - // Use the ref to get the most recent graphSpec, avoiding stale closure issues - const result = handleAggregatorConnection( - latestGraphSpecRef.current, - connection, - updateNodeInternals, - ); - - // Update both the ref and the state with the new spec (which includes the new input) - latestGraphSpecRef.current = result.graphSpec; - updateGraphSpec(result.graphSpec); - - // For aggregator connections, if we should create an edge, do it in the next tick - // after React Flow has processed the node updates - if ( - isAggregatorConnection && - result.shouldCreateEdge && - result.redirectedConnection - ) { - const targetNodeId = connection.target; - const redirectedConn = result.redirectedConnection; - - // Update node internals multiple times to ensure React Flow sees the new handles - updateNodeInternals(targetNodeId); - - // Create the edge connection after a slight delay - requestAnimationFrame(() => { - updateNodeInternals(targetNodeId); - - const finalGraphSpec = handleConnection( - latestGraphSpecRef.current, - redirectedConn, - ); - latestGraphSpecRef.current = finalGraphSpec; - updateGraphSpec(finalGraphSpec); - - // One more update to ensure edge renders properly - requestAnimationFrame(() => { - updateNodeInternals(targetNodeId); - }); - }); - } - }; - - const handleGhostDrop = ( - finalConnectionState: FinalConnectionState | null, - ) => { - const fromNode = finalConnectionState?.fromNode; - const fromHandle = finalConnectionState?.fromHandle; - - if (!fromNode || !fromHandle?.id) { - return false; - } - - if (!FAST_PLACE_NODE_TYPES.has(fromNode.type)) { - return false; - } - - const position = computeDropPositionFromRefs( - ghostNodeRef.current, - latestFlowPosRef.current, - fromHandle.type, - store.getState(), - ); - - if (!position) { - return false; - } - - if (fromHandle.type !== "source" && fromHandle.type !== "target") { - return false; - } - - const ioType: GhostNodeData["ioType"] = - fromHandle.type === "source" ? "output" : "input"; - const updatedSubgraphSpec = createConnectedIONode({ - componentSpec: currentSubgraphSpec, - taskNodeId: fromHandle.nodeId, - handleId: fromHandle.id, - position, - ioType, - }); - - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - setComponentSpec(updatedRootSpec); - updateReactFlow(updatedRootSpec); - shouldCreateIONodeRef.current = false; - - return true; - }; - - const onConnectEnd = ( - e: MouseEvent | TouchEvent, - connectionState: FinalConnectionState, - ) => { - if (connectionState.isValid) { - return; - } - - if (!(e instanceof MouseEvent)) { - return; - } - - if (!shouldCreateIONodeRef.current) { - return; - } - - handleGhostDrop(connectionState); - }; - - useEffect(() => { - function handleMouseMove(event: MouseEvent) { - if (isConnecting && reactFlowInstance) { - const flowPos = reactFlowInstance.screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); - latestFlowPosRef.current = flowPos; - } - } - if (isConnecting) { - window.addEventListener("mousemove", handleMouseMove); - } - return () => { - window.removeEventListener("mousemove", handleMouseMove); - }; - }, [isConnecting, reactFlowInstance]); - - const onDragOver = async (event: DragEvent) => { - event.preventDefault(); - - const hasFiles = event.dataTransfer.types.includes("Files"); - if (hasFiles) { - return; - } - - event.dataTransfer.dropEffect = "move"; - - const cursorPosition = reactFlowInstance?.screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); - - if (cursorPosition) { - const hoveredNode = nodes.find((node) => - isPositionInNode(node, cursorPosition), - ); - - if (!hoveredNode && replaceTarget) { - setReplaceTarget(null); - return; - } - - if (!hoveredNode || hoveredNode.id === replaceTarget?.id) return; - - if (hoveredNode.type && !REPLACEABLE_NODES.has(hoveredNode.type)) { - setReplaceTarget(null); - return; - } - - setReplaceTarget(hoveredNode); - } - }; - - const onDrop = async (event: DragEvent) => { - event.preventDefault(); - if (readOnly) return; - - if (event.dataTransfer.files.length > 0) { - try { - // Parse the imported YAML to get the component spec - const content = await readTextFromFile(event.dataTransfer.files[0]); - const hydratedComponentRef = await hydrateComponentReference({ - text: content, - }); - - if (!hydratedComponentRef) { - notify( - "Failed to parse component spec from imported content", - "error", - ); - return; - } - - const result = await addToComponentLibrary( - hydratedComponentRef, - "canvas_file_drop", - ); - - if (result && reactFlowInstance) { - const position = getPositionFromEvent(event, reactFlowInstance); - const taskSpec: TaskSpec = { - annotations: {}, - componentRef: hydratedComponentRef, - }; - const { spec: newComponentSpec } = addTask( - "task", - taskSpec, - position, - componentSpec, - ); - - setComponentSpec(newComponentSpec); - - const ref = hydratedComponentRef; - if (ref.digest && ref.spec?.name) { - addRecentlyUsed({ - type: "component", - id: ref.digest, - name: ref.spec.name, - }); - } - - track("pipeline_editor.component.dropped", { - ...componentMetadata(hydratedComponentRef, "file"), - drop_kind: "file_import", - pipeline_id: componentSpec.name, - }); - } - } catch (error) { - console.error("Failed to add imported component to canvas:", error); - notify("Failed to add component to canvas", "error"); - } - - return; - } - - const { spec: droppedTask, nodeType } = getNodeFromEvent(event); - - if (!nodeType) { - console.error("Dropped node type not identified."); - return; - } - - if (nodeType === "flex" && reactFlowInstance) { - const position = getPositionFromEvent(event, reactFlowInstance); - - const { spec: updatedSubgraphSpec } = addFlexNode( - position, - currentUserDetails?.id || "unknown", - currentSubgraphSpec, - ); - - const newRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(newRootSpec); - return; - } - - if (!isTaskNodeType(nodeType)) { - console.error("Dropped node type is not supported:", nodeType); - return; - } - - if (!droppedTask && nodeType === "task") { - console.error("Unable to find dropped task."); - return; - } - - if (isNotMaterializedComponentReference(droppedTask?.componentRef)) { - const hydratedComponentRef = await hydrateComponentReference( - droppedTask.componentRef, - ); - - if (hydratedComponentRef) { - droppedTask.componentRef = hydratedComponentRef; - } else { - notify( - "Failed to add component to canvas. Please, try again.", - "error", - ); - return; - } - } - - if (replaceTarget) { - if (!droppedTask) { - console.error( - "Replacement by Input or Output node is currently unsupported.", - ); - return; - } - - const targetTaskId = replaceTarget.data.taskId as string; - - const { updatedGraphSpec, lostInputs, newTaskId } = replaceTaskNode( - targetTaskId, - droppedTask.componentRef, - currentGraphSpec, - ); - - const dialogData = getReplaceConfirmationDetails( - replaceTarget, - newTaskId, - lostInputs, - ); - - const confirmed = await triggerConfirmation(dialogData); - - const fromComponentRef = - currentGraphSpec.tasks[targetTaskId]?.componentRef; - - setReplaceTarget(null); - - if (confirmed) { - updateGraphSpec(updatedGraphSpec); - - track("pipeline_editor.component.replaced", { - from_component_id: fromComponentRef?.digest, - from_component_name: fromComponentRef - ? componentMetadata(fromComponentRef).component_name - : undefined, - to_component_id: droppedTask.componentRef?.digest, - to_component_name: droppedTask.componentRef - ? componentMetadata(droppedTask.componentRef).component_name - : undefined, - lost_inputs_count: lostInputs.length, - pipeline_id: componentSpec.name, - }); - } - - return; - } - - if (reactFlowInstance) { - const position = getPositionFromEvent(event, reactFlowInstance); - - const { spec: newSubgraphSpec } = addTask( - nodeType, - droppedTask, - position, - currentSubgraphSpec, - ); - - const newRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - newSubgraphSpec, - ); - - setComponentSpec(newRootSpec); - - const ref = droppedTask?.componentRef; - if (ref?.digest && ref.spec?.name) { - addRecentlyUsed({ - type: "component", - id: ref.digest, - name: ref.spec.name, - }); - } - - if (ref) { - track("pipeline_editor.component.dropped", { - ...componentMetadata(ref, "library"), - drop_kind: "new_node", - pipeline_id: componentSpec.name, - }); - } - } - }; - - const onRemoveNodes = async () => { - const nodes = getSelectedNodes(); - const confirmed = await triggerConfirmation( - getDeleteConfirmationDetails({ nodes, edges: [] }), - ); - if (confirmed) { - onElementsRemove({ nodes, edges: [] }); - } - }; - - const handleOnNodesChange = (changes: NodeChange[]) => { - const positionChanges = changes.filter( - (change) => change.type === "position" && change.dragging === false, - ); - - if (positionChanges.length > 0) { - const updatedNodes = positionChanges - .map((change) => { - if ("id" in change && "position" in change && change.position) { - const node = nodes.find((n) => n.id === change.id); - return node - ? { - ...node, - position: { x: change.position.x, y: change.position.y }, - } - : null; - } - return null; - }) - .filter(Boolean) as Node[]; - - if (updatedNodes.length > 0) { - const updatedSubgraphSpec = updateNodePositions( - updatedNodes, - currentSubgraphSpec, - ); - - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(updatedRootSpec); - } - } - - onNodesChange(changes); - }; - - const handleBeforeDelete = async (params: NodesAndEdges) => { - if (readOnly) { - return false; - } - - if (params.nodes.length === 0 && params.edges.length === 0) { - return false; - } - - if (shiftKeyPressed) { - return true; - } - - const confirmed = await triggerConfirmation( - getDeleteConfirmationDetails(params), - ); - - return confirmed; - }; - - const onDuplicateNodes = () => { - const { - updatedComponentSpec: updatedSubgraphSpec, - newNodes, - updatedNodes, - } = duplicateNodes(currentSubgraphSpec, getSelectedNodes(), { - selected: true, - author: currentUserDetails?.id, - }); - - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(updatedRootSpec); - - updateOrAddNodes({ - updatedNodes, - newNodes, - }); - }; - - const onUpgradeNodes = async () => { - let newGraphSpec = currentGraphSpec; - const allLostInputs: InputSpec[] = []; - const includedNodes: Node[] = []; - const excludedNodes: Node[] = []; - - getSelectedNodes().forEach((node) => { - if (node.type && !UPGRADEABLE_NODES.has(node.type)) { - excludedNodes.push(node); - return; - } - - const taskSpec = node.data.taskSpec as TaskSpec | undefined; - // Custom components don't have a componentRef.url so they are currently excluded from bulk operations - if (taskSpec?.componentRef && taskSpec.componentRef.url) { - const { updatedGraphSpec, lostInputs } = replaceTaskNode( - node.data.taskId as string, - taskSpec.componentRef, - newGraphSpec, - ); - - if (lostInputs.length > 0) { - allLostInputs.push(...lostInputs); - } - - includedNodes.push(node); - newGraphSpec = { ...updatedGraphSpec }; - } else { - excludedNodes.push(node); - } - }); - - if (includedNodes.length === 0) { - notify("Selected nodes are not upgradeable", "info"); - return; - } - - const dialogData = getBulkUpdateConfirmationDetails( - includedNodes, - excludedNodes, - allLostInputs, - ); - - const confirmed = await triggerConfirmation(dialogData); - - if (confirmed) { - updateGraphSpec(newGraphSpec); - notify(`${includedNodes.length} nodes updated`, "success"); - } - }; - - const onGroupNodes = async () => { - if (!canGroup) return; - setShowNewSubgraphDialog(true); - }; - - const handleCreateSubgraph = async (activeNodes: Node[], name: string) => { - const onSuccess = (updatedComponentSpec: ComponentSpec) => { - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedComponentSpec, - ); - - setComponentSpec(updatedRootSpec); - - setNodes((nodes) => deselectAllNodes(nodes)); - }; - - const onError = (error: Error) => { - console.error("Failed to create subgraph:", error); - notify("Failed to create subgraph", "error"); - }; - - await handleGroupNodes( - activeNodes, - currentSubgraphSpec, - name, - onSuccess, - onError, - ); - }; - - const handleSelectionChange = () => { - if (selectedNodes.length < 1) { - setShowToolbar(false); - } - }; - - const handleSelectionStart = () => { - isBoxSelecting.current = true; - }; - - const handleSelectionEnd = () => { - isBoxSelecting.current = false; - setShowToolbar(true); - }; - - useEffect(() => { - preserveIOSelectionOnSpecChange(componentSpec); - updateReactFlow(componentSpec); - initialCanvasLoaded.current = true; - }, [ - replaceTarget, - componentSpec, - currentSubgraphPath, - preserveIOSelectionOnSpecChange, - ]); - - useEffect(() => { - if (!reactFlowInstance) return; - - if (!initialFitDone.current) { - initialFitDone.current = true; - - if (!fitViewOnInit) return; - } - - reactFlowInstance.fitView({ maxZoom: 1, duration: 300 }); - }, [currentSubgraphPath, reactFlowInstance, fitViewOnInit]); - - // Reset when loading a new component file - useEffect(() => { - resetPrevSpec(); - }, [componentSpec?.name, resetPrevSpec]); - - const fitView = () => { - reactFlowInstance?.fitView({ maxZoom: 1 }); - }; - - useScheduleExecutionOnceWhenConditionMet( - fitViewOnInit && initialCanvasLoaded.current && !!reactFlowInstance, - fitView, - ); - - const onCopy = () => { - const nodes = getSelectedNodes(); - if (nodes.length > 0) { - const selectedNodesJson = JSON.stringify(nodes); - navigator.clipboard.writeText(selectedNodesJson).catch((err) => { - console.error("Failed to copy nodes to clipboard:", err); - }); - const message = `Copied ${nodes.length} nodes to clipboard`; - notify(message, "success"); - } - }; - - const onPaste = () => { - if (readOnly) return; - - // Paste nodes from clipboard to the centre of the Canvas - navigator.clipboard.readText().then((clipboardText) => { - try { - let parsedData; - try { - parsedData = JSON.parse(clipboardText); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (err) { - return; - } - - const nodesToPaste: Node[] = parsedData; - - const { domNode } = store.getState(); - const boundingRect = domNode?.getBoundingClientRect(); - - if (boundingRect) { - const center = reactFlowInstance?.screenToFlowPosition({ - x: boundingRect.x + boundingRect.width / 2, - y: boundingRect.y + boundingRect.height / 2, - }); - - const reactFlowCenter = { - x: center?.x || 0, - y: center?.y || 0, - }; - - const { newNodes, updatedComponentSpec: updatedSubgraphSpec } = - duplicateNodes(currentSubgraphSpec, nodesToPaste, { - position: reactFlowCenter, - connection: "internal", - author: currentUserDetails?.id, - }); - - const updatedNodes = nodes.map((node) => ({ - ...node, - selected: false, - })); - - updateOrAddNodes({ - updatedNodes, - newNodes, - }); - - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(updatedRootSpec); - } - } catch (err) { - console.error("Failed to paste nodes from clipboard:", err); - } - }); - }; - - useCopyPaste({ - onCopy, - onPaste, - }); - - const onPaneClick = () => { - clearContent(); - }; - - const getSelectionMode = () => { - if (shiftKeyPressed && metaKeyPressed) { - return SelectionMode.Partial; - } - - return SelectionMode.Full; - }; - - const selectionMode = getSelectionMode(); - - const applyAutoLayout = ( - nodesToLayout: Node[], - edgesToConsider: Edge[], - algorithm: LayoutAlgorithm = "sugiyama", - ) => { - const layoutedNodes = autoLayoutNodes( - nodesToLayout, - edgesToConsider, - algorithm, - ); - - const updatedSubgraphSpec = updateNodePositions( - layoutedNodes, - currentSubgraphSpec, - ); - - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(updatedRootSpec); - }; - - const handleAutoLayout = async (algorithm: LayoutAlgorithm) => { - applyAutoLayout(nodes, edges, algorithm); - - requestAnimationFrame(() => { - reactFlowInstance?.fitView({ - maxZoom: 1, - duration: 300, - }); - }); - }; - - const onAutoLayout = () => { - const nodes = getSelectedNodes(); - const connectedEdges = edges.filter( - (edge) => - nodes.some((node) => node.id === edge.source) || - nodes.some((node) => node.id === edge.target), - ); - - applyAutoLayout(nodes, connectedEdges); - }; - - useImperativeHandle( - ref, - () => ({ - autoLayout: handleAutoLayout, - }), - [handleAutoLayout], - ); - - const multiSelectCallbacks = { - onDelete: !readOnly ? onRemoveNodes : undefined, - onDuplicate: !readOnly ? onDuplicateNodes : undefined, - onUpgrade: !readOnly && canUpgrade ? onUpgradeNodes : undefined, - onGroup: !readOnly && canGroup ? onGroupNodes : undefined, - onCopy: onCopy, - onAutoLayout: onAutoLayout, - }; - - useEffect(() => { - const nodes = getSelectedNodes(); - if (nodes.length > 1) { - setContent( - , - ); - setOpen(true); - } else if (nodes.length === 0) { - clearContent(); - } - }, [currentSubgraphSpec, selectedNodeIds, readOnly, canUpgrade, canGroup]); - - return ( - - - - node.id)} - isVisible={showToolbar} - offset={0} - align="end" - className="z-9999!" - > - - - {children} - - - confirmationHandlers?.onConfirm()} - onCancel={() => confirmationHandlers?.onCancel()} - /> - setShowNewSubgraphDialog(false)} - selectedNodes={selectedNodes} - currentSubgraphSpec={currentSubgraphSpec} - onCreateSubgraph={handleCreateSubgraph} - /> - - ); -}; - -FlowCanvas.displayName = "FlowCanvas"; - -export default FlowCanvas; diff --git a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/GhostNode.tsx b/src/components/shared/ReactFlow/FlowCanvas/GhostNode/GhostNode.tsx deleted file mode 100644 index 6ec0418d6c..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/GhostNode.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { type Node, type NodeProps, Position } from "@xyflow/react"; -import { memo, useMemo } from "react"; - -import { cn } from "@/lib/utils"; - -import IONode from "../IONode/IONode"; -import type { GhostNodeData } from "./types"; -import { GHOST_NODE_BASE_OFFSET_X, GHOST_NODE_BASE_OFFSET_Y } from "./utils"; - -type GhostNodeProps = NodeProps>; - -const GhostNode = ({ data, id }: GhostNodeProps) => { - const { ioType, label, dataType, value, defaultValue } = data; - - const side = ioType === "input" ? Position.Left : Position.Right; - const transformOrigin = - side === Position.Left ? "center right" : "center left"; - const offsetX = GHOST_NODE_BASE_OFFSET_X; - const offsetY = GHOST_NODE_BASE_OFFSET_Y; - - const ghostNodeData = useMemo( - () => ({ - label, - type: dataType ?? "any", - value, - default: defaultValue, - readOnly: true, - }), - [label, dataType, value, defaultValue], - ); - - return ( - - - - - - ); -}; - -export default memo(GhostNode); diff --git a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/types.ts b/src/components/shared/ReactFlow/FlowCanvas/GhostNode/types.ts deleted file mode 100644 index 941ccfc6a5..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface GhostNodeData extends Record { - ioType: "input" | "output"; - label: string; - dataType?: string; - value?: string; - defaultValue?: string; - connectedTaskLabel?: string; - connectedOutputName?: string; -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/GhostNode/utils.ts deleted file mode 100644 index c074a4f137..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/GhostNode/utils.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { - Edge, - Handle, - Node, - ReactFlowState, - XYPosition, -} from "@xyflow/react"; - -import { DEFAULT_IO_NODE_WIDTH } from "@/components/shared/ReactFlow/FlowCanvas/IONode/IONode"; - -import { EdgeColor } from "../Edges/utils"; -import type { GhostNodeData } from "./types"; - -export const GHOST_NODE_ID = "ghost-node"; - -export const GHOST_NODE_BASE_OFFSET_X = -12; -export const GHOST_NODE_BASE_OFFSET_Y = -24; - -type CreateGhostNodeParams = { - position: XYPosition; - ioType: GhostNodeData["ioType"]; - label: string; - dataType?: string; - value?: string; - defaultValue?: string; - connectedTaskLabel?: string; - connectedOutputName?: string; -}; - -export const createGhostNode = ({ - position, - ioType, - label, - dataType, - value, - defaultValue, - connectedTaskLabel, - connectedOutputName, -}: CreateGhostNodeParams): Node => ({ - id: GHOST_NODE_ID, - type: "ghost", - position, - data: { - ioType, - label, - dataType: dataType ?? "any", - value, - defaultValue, - connectedTaskLabel, - connectedOutputName, - }, - draggable: false, - selectable: false, - deletable: false, - connectable: false, - focusable: false, - zIndex: 1000, -}); - -const getGhostNodeDropPosition = ( - position: XYPosition | null, - ioType: GhostNodeData["ioType"], - nodeWidth: number, -): XYPosition | null => { - if (!position) { - return null; - } - - if (ioType === "input") { - return { - x: position.x + GHOST_NODE_BASE_OFFSET_X - nodeWidth, - y: position.y + GHOST_NODE_BASE_OFFSET_Y, - }; - } - - return { - x: position.x + GHOST_NODE_BASE_OFFSET_X, - y: position.y + GHOST_NODE_BASE_OFFSET_Y, - }; -}; - -/** - * Computes the final drop position for creating an IO node from a ghost node preview. - * Uses ghost node position if available, falls back to latest flow position. - * Adjusts position based on IO type to properly align the created node. - */ -export const computeDropPositionFromRefs = ( - ghostNode: Node | null, - latestFlowPosition: XYPosition | null, - fromHandleType: "source" | "target" | null | undefined, - reactFlowState: ReactFlowState, -): XYPosition | null => { - const basePosition = ghostNode?.position ?? latestFlowPosition; - if (!basePosition) { - return null; - } - - const dropIoType = - ghostNode?.data.ioType ?? - (fromHandleType === "target" ? "input" : "output"); - - let width = DEFAULT_IO_NODE_WIDTH; - - if (ghostNode) { - // Get actual ghost node dimensions from React Flow state - const { nodeLookup } = reactFlowState; - const node = nodeLookup.get(ghostNode.id); - - if (node?.measured.width && node?.measured.height) { - width = node.measured.width; - } - } - - return dropIoType - ? (getGhostNodeDropPosition(basePosition, dropIoType, width) ?? - basePosition) - : basePosition; -}; - -export const getGhostHandleId = (): string => { - return `${GHOST_NODE_ID}-handle`; -}; - -export const createGhostEdge = (connectionSourceHandle: Handle): Edge => { - const isFromSource = connectionSourceHandle.type === "source"; - const ghostHandleId = getGhostHandleId(); - - const edgeColor = isFromSource ? EdgeColor.Output : EdgeColor.Input; - - const ghostEdge = { - id: "ghost-edge", - source: isFromSource ? connectionSourceHandle.nodeId : GHOST_NODE_ID, - sourceHandle: isFromSource ? connectionSourceHandle.id : ghostHandleId, - target: isFromSource ? GHOST_NODE_ID : connectionSourceHandle.nodeId, - targetHandle: isFromSource ? ghostHandleId : connectionSourceHandle.id, - markerStart: undefined, - markerEnd: undefined, - style: { - stroke: edgeColor, - strokeWidth: 4, - strokeDasharray: "5,5", - strokeOpacity: 0.5, - }, - animated: true, - }; - - return ghostEdge; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx b/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx deleted file mode 100644 index f4473d4b88..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { Handle, Position, useEdges, useReactFlow } from "@xyflow/react"; -import type { MouseEvent } from "react"; -import { memo, useCallback, useEffect, useMemo } from "react"; - -import { InputValueEditor } from "@/components/Editor/IOEditor/InputValueEditor"; -import { OutputNameEditor } from "@/components/Editor/IOEditor/OutputNameEditor"; -import type { OutputConnectedDetails } from "@/components/Editor/utils/getOutputConnectedDetails"; -import { getOutputConnectedDetails } from "@/components/Editor/utils/getOutputConnectedDetails"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { QuickTooltip } from "@/components/ui/tooltip"; -import { Paragraph } from "@/components/ui/typography"; -import { useEdgeSelectionHighlight } from "@/hooks/useEdgeSelectionHighlight"; -import { useExecutionArtifacts } from "@/hooks/useExecutionArtifacts"; -import { useIsMultiSelect } from "@/hooks/useIsMultiSelect"; -import { cn } from "@/lib/utils"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useContextPanel } from "@/providers/ContextPanelProvider"; -import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; -import type { OutputSpec } from "@/utils/componentSpec"; -import { getArgumentValue } from "@/utils/nodes/taskArguments"; -import { isViewingSubgraph } from "@/utils/subgraphUtils"; - -import { getGhostHandleId, GHOST_NODE_ID } from "../GhostNode/utils"; - -export const DEFAULT_IO_NODE_WIDTH = 300; - -interface IONodeProps { - id: string; - type: "input" | "output"; - data: { - label: string; - value?: string; - default?: string; - type?: string; - readOnly?: boolean; - description?: string; - }; - selected: boolean; - deletable: boolean; -} - -const IONode = ({ id, type, data, selected = false }: IONodeProps) => { - const { currentGraphSpec, currentSubgraphSpec, currentSubgraphPath } = - useComponentSpec(); - - const executionData = useExecutionDataOptional(); - const taskArguments = executionData?.rootDetails?.task_spec.arguments; - const rootExecutionId = executionData?.rootExecutionId; - - const { - setContent, - clearContent, - setOpen: setContextPanelOpen, - } = useContextPanel(); - - const edges = useEdges(); - const { setEdges } = useReactFlow(); - const { isConnectedToSelectedEdge, hasAnySelectedEdge } = - useEdgeSelectionHighlight(id); - - const isDimmed = hasAnySelectedEdge && !isConnectedToSelectedEdge; - - const isInput = type === "input"; - const isOutput = type === "output"; - - const handleHandleClick = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - - const connectedEdgeIds = new Set( - edges - .filter((edge) => (isInput ? edge.source === id : edge.target === id)) - .map((edge) => edge.id), - ); - - if (connectedEdgeIds.size === 0) return; - - setEdges((eds) => - eds.map((edge) => ({ - ...edge, - selected: connectedEdgeIds.has(edge.id), - })), - ); - }, - [edges, id, isInput, setEdges], - ); - - const readOnly = !!data.readOnly; - - const { isMultiSelect, isMultiSelectRef } = useIsMultiSelect(); - - const isInSubgraph = isViewingSubgraph(currentSubgraphPath); - - const handleType = isInput ? "source" : "target"; - const handlePosition = isInput ? Position.Right : Position.Left; - const bgColor = isInput ? "bg-io-input" : "bg-io-output"; - const selectedBorderColor = isInput - ? "border-blue-500 dark:ring-2 dark:ring-blue-400/60" - : "border-violet-500 dark:ring-2 dark:ring-violet-400/60"; - const defaultBorderColor = isInput - ? "border-blue-300 dark:border-blue-500" - : "border-violet-300 dark:border-violet-500"; - const edgeHighlightClasses = - "border-edge-selected! ring-2 ring-edge-selected/30"; - - const borderColor = cn( - bgColor, - isConnectedToSelectedEdge - ? edgeHighlightClasses - : selected - ? selectedBorderColor - : defaultBorderColor, - ); - - const input = useMemo( - () => - currentSubgraphSpec.inputs?.find((input) => input.name === data.label), - [currentSubgraphSpec.inputs, data.label], - ); - - const inputTaskArgument = getArgumentValue(taskArguments, input?.name); - - const output = useMemo( - () => - currentSubgraphSpec.outputs?.find((output) => output.name === data.label), - [currentSubgraphSpec.outputs, data.label], - ); - - useEffect(() => { - if (selected && !isMultiSelect) { - if (input && isInput) { - setContent( - , - ); - } - - if (output && isOutput) { - const outputConnectedDetails = getOutputConnectedDetails( - currentGraphSpec, - output.name, - ); - - setContent( - , - ); - } - - setContextPanelOpen(true); - } - - return () => { - if (selected && !isMultiSelectRef.current) { - clearContent(); - } - }; - }, [input, output, selected, isMultiSelect, readOnly]); - - const connectedOutput = getOutputConnectedDetails( - currentGraphSpec, - data.label, - ); - const outputConnectedValue = connectedOutput.outputName; - const outputConnectedType = connectedOutput.outputType; - const outputConnectedTaskName = connectedOutput.taskName; - - const handleDefaultClassName = - "w-3! h-3! border-0! transform-none! bg-gray-500!"; - - const handleClassName = isInput ? "translate-x-1.5" : "-translate-x-1.5"; - - const hasDataValue = !!data.value || !!inputTaskArgument; - const hasDataDefault = !!data.default; - - const inputValue = hasDataValue - ? (inputTaskArgument ?? data.value) - : hasDataDefault - ? data.default - : isInSubgraph - ? "→subgraph arguments" - : null; - - const outputValue = outputConnectedValue ?? data.value ?? null; - - const value = isInput ? inputValue : outputValue; - - // The rest of our code doesn't support IO Handles having ids (yet). - // For now, only assign an id to the Handle if this is a ghost node. - const handleId = id === GHOST_NODE_ID ? getGhostHandleId() : undefined; - - return ( - - - - {data.label} - - {!!data.description && ( - - - {data.description} - - - )} - - - - {/* type */} - - Type:{" "} - {outputConnectedType ?? data.type ?? "Any"} - - - {!!outputConnectedTaskName && ( - - {outputConnectedTaskName} - - )} - - {/* value */} - - - Value: - - - {value ?? "No value"} - - - - - - - - - ); -}; - -export default memo(IONode); - -interface OutputNodeContentProps { - output: OutputSpec; - connectedDetails: OutputConnectedDetails; - disabled: boolean; - rootExecutionId: string | undefined; -} - -const OutputNodeContent = ({ - output, - connectedDetails, - disabled, - rootExecutionId, -}: OutputNodeContentProps) => { - const { data: artifacts } = useExecutionArtifacts(rootExecutionId); - - const artifact = artifacts?.output_artifacts?.[output.name]; - - return ( - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/FlexDetailPanel.tsx b/src/components/shared/ReactFlow/FlowCanvas/Multiselect/FlexDetailPanel.tsx deleted file mode 100644 index c3e353f786..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/FlexDetailPanel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Node } from "@xyflow/react"; - -import { InfoBox } from "@/components/shared/InfoBox"; -import { Paragraph } from "@/components/ui/typography"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; - -import { FlexNodeEditor } from "../FlexNode/FlexNodeEditor"; -import { getFlexNode } from "../FlexNode/interface"; - -export const FlexDetailPanel = ({ - node, - readOnly, -}: { - node: Node; - readOnly: boolean; -}) => { - const { currentSubgraphSpec } = useComponentSpec(); - const flexNode = getFlexNode(node.id, currentSubgraphSpec); - - if (!flexNode) { - return ( - - - Invalid data for Sticky Note. - - - ); - } - - return ; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/InputDetailPanel.tsx b/src/components/shared/ReactFlow/FlowCanvas/Multiselect/InputDetailPanel.tsx deleted file mode 100644 index aa502cae32..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/InputDetailPanel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Node } from "@xyflow/react"; - -import { InputValueEditor } from "@/components/Editor/IOEditor/InputValueEditor"; -import { InfoBox } from "@/components/shared/InfoBox"; -import { Paragraph } from "@/components/ui/typography"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { nodeIdToInputName } from "@/utils/nodes/nodeIdUtils"; - -export const InputDetailPanel = ({ - node, - readOnly, -}: { - node: Node; - readOnly: boolean; -}) => { - const { currentSubgraphSpec } = useComponentSpec(); - const name = nodeIdToInputName(node.id); - const input = currentSubgraphSpec.inputs?.find((i) => i.name === name); - - if (!input) { - return ( - - - {`Input "${name}" not found in pipeline spec.`} - - - ); - } - - return ; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/MultiSelectPanel.tsx b/src/components/shared/ReactFlow/FlowCanvas/Multiselect/MultiSelectPanel.tsx deleted file mode 100644 index 9cc107158f..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/MultiSelectPanel.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import type { Node } from "@xyflow/react"; -import { useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Heading, Text } from "@/components/ui/typography"; - -import { ActionButton } from "../../../Buttons/ActionButton"; -import { ContentBlock } from "../../../ContextPanel/Blocks/ContentBlock"; -import { FlexDetailPanel } from "./FlexDetailPanel"; -import { InputDetailPanel } from "./InputDetailPanel"; -import { OutputDetailPanel } from "./OutputDetailPanel"; -import { TaskDetailPanel } from "./TaskDetailPanel"; -import { getNodeDisplayName, getNodeTypeIcon, getNodeTypeLabel } from "./utils"; - -interface MultiSelectPanelProps { - selectedNodes: Node[]; - readOnly: boolean; - onCopy?: () => void; - onDuplicate?: () => void; - onDelete?: () => void; - onUpgrade?: () => void; - onGroup?: () => void; - onAutoLayout?: () => void; -} - -export const MultiSelectPanel = ({ - selectedNodes, - readOnly, - onCopy, - onDuplicate, - onDelete, - onUpgrade, - onGroup, - onAutoLayout, -}: MultiSelectPanelProps) => { - const [detailNodeId, setDetailNodeId] = useState(null); - - const detailNode = selectedNodes.find((n) => n.id === detailNodeId); - - if (detailNode) { - return ( - setDetailNodeId(null)} - /> - ); - } - - return ( - - {selectedNodes.length} nodes selected - - - - {selectedNodes.map((node) => { - const iconName = getNodeTypeIcon(node.type); - const typeLabel = getNodeTypeLabel(node.type); - const displayName = getNodeDisplayName(node); - - return ( - setDetailNodeId(node.id)} - > - - - - - {displayName} - - - - - - {typeLabel} - - - - - - ); - })} - - - - - - {onUpgrade && ( - - )} - {onGroup && ( - - )} - {onDuplicate && ( - - )} - {onCopy && ( - - )} - {onAutoLayout && ( - - )} - {onDelete && ( - - )} - - - - ); -}; - -const NodeDetail = ({ - node, - readOnly, - onBack, -}: { - node: Node; - readOnly: boolean; - onBack: () => void; -}) => { - return ( - - - - Back to selection - - {node.type === "task" && } - {node.type === "input" && ( - - )} - {node.type === "output" && ( - - )} - {node.type === "flex" && ( - - )} - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/OutputDetailPanel.tsx b/src/components/shared/ReactFlow/FlowCanvas/Multiselect/OutputDetailPanel.tsx deleted file mode 100644 index da3fcf4b13..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/OutputDetailPanel.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { Node } from "@xyflow/react"; - -import { OutputNameEditor } from "@/components/Editor/IOEditor/OutputNameEditor"; -import { getOutputConnectedDetails } from "@/components/Editor/utils/getOutputConnectedDetails"; -import { InfoBox } from "@/components/shared/InfoBox"; -import { Paragraph } from "@/components/ui/typography"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { nodeIdToOutputName } from "@/utils/nodes/nodeIdUtils"; - -export const OutputDetailPanel = ({ - node, - readOnly, -}: { - node: Node; - readOnly: boolean; -}) => { - const { currentSubgraphSpec, currentGraphSpec } = useComponentSpec(); - const name = nodeIdToOutputName(node.id); - const output = currentSubgraphSpec.outputs?.find((o) => o.name === name); - - if (!output) { - return ( - - - {`Output "${name}" not found in pipeline spec.`} - - - ); - } - - const connectedDetails = getOutputConnectedDetails(currentGraphSpec, name); - - return ( - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/SelectionToolbar.tsx b/src/components/shared/ReactFlow/FlowCanvas/Multiselect/SelectionToolbar.tsx deleted file mode 100644 index 524f312e77..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/SelectionToolbar.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { icons } from "lucide-react"; - -import TooltipButton from "@/components/shared/Buttons/TooltipButton"; -import { Icon } from "@/components/ui/icon"; -import { InlineStack } from "@/components/ui/layout"; -import { cn } from "@/lib/utils"; - -interface SelectionToolbarProps { - onCopy?: () => void; - onDuplicate?: () => void; - onDelete?: () => void; - onUpgrade?: () => void; - onGroup?: () => void; - onAutoLayout?: () => void; -} - -const SelectionToolbar = ({ - onCopy, - onDuplicate, - onDelete, - onUpgrade, - onGroup, - onAutoLayout, -}: SelectionToolbarProps) => { - return ( - - - - - - - - - ); -}; - -export default SelectionToolbar; - -const ToolbarButton = ({ - name, - icon, - dangerous, - callback, - testId, -}: { - name: string; - icon: keyof typeof icons; - dangerous?: boolean; - callback?: () => void; - testId?: string; -}) => { - if (!callback) { - return null; - } - - const baseStyle = "h-full aspect-square w-min rounded-sm p-1"; - - return ( - - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/TaskDetailPanel.tsx b/src/components/shared/ReactFlow/FlowCanvas/Multiselect/TaskDetailPanel.tsx deleted file mode 100644 index a1e540262f..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/TaskDetailPanel.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { Node } from "@xyflow/react"; - -import { InfoBox } from "@/components/shared/InfoBox"; -import { Paragraph } from "@/components/ui/typography"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; -import { TaskNodeProvider, useTaskNode } from "@/providers/TaskNodeProvider"; -import type { TaskNodeData } from "@/types/taskNode"; -import { isGraphImplementation } from "@/utils/componentSpec"; -import { nodeIdToTaskId } from "@/utils/nodes/nodeIdUtils"; - -import TaskOverview from "../TaskNode/TaskOverview"; - -export const TaskDetailPanel = ({ node }: { node: Node }) => { - const { currentSubgraphSpec } = useComponentSpec(); - const executionData = useExecutionDataOptional(); - - if (!isGraphImplementation(currentSubgraphSpec.implementation)) { - return ( - - - Unable to load task details. Graph specification is invalid. - - - ); - } - - const taskId = nodeIdToTaskId(node.id); - const taskSpec = currentSubgraphSpec.implementation.graph.tasks?.[taskId]; - - if (!taskId || !taskSpec) { - return ( - - - Unable to load task details. Missing task ID or task specification. - - - ); - } - - const status = executionData?.taskExecutionStatusMap.get(taskId); - const freshData: TaskNodeData = { ...node.data, taskSpec }; - - return ( - - - - ); -}; - -const TaskDetailContent = () => { - const taskNode = useTaskNode(); - return ; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/Multiselect/utils.ts deleted file mode 100644 index e6274a75a3..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Multiselect/utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Node } from "@xyflow/react"; -import type { icons } from "lucide-react"; - -import { getTaskDisplayName } from "@/utils/getComponentName"; -import { - nodeIdToInputName, - nodeIdToOutputName, - nodeIdToTaskId, -} from "@/utils/nodes/nodeIdUtils"; - -import { isFlexNodeData } from "../FlexNode/types"; -import { getFlexNodeDisplayName } from "../FlexNode/utils"; - -export function getNodeTypeIcon( - nodeType: string | undefined, -): keyof typeof icons { - switch (nodeType) { - case "task": - return "Boxes"; - case "input": - return "SquareArrowRightEnter"; - case "output": - return "SquareArrowRightExit"; - case "flex": - return "StickyNote"; - default: - return "Circle"; - } -} - -export function getNodeTypeLabel(nodeType: string | undefined): string { - switch (nodeType) { - case "task": - return "Task"; - case "input": - return "Input"; - case "output": - return "Output"; - case "flex": - return "Note"; - default: - return nodeType ?? ""; - } -} - -export function getNodeDisplayName(node: Node): string { - if (node.type === "task") return getTaskDisplayName(nodeIdToTaskId(node.id)); - if (node.type === "input") return nodeIdToInputName(node.id); - if (node.type === "output") return nodeIdToOutputName(node.id); - if (node.type === "flex") - return isFlexNodeData(node.data) - ? getFlexNodeDisplayName(node.data) - : node.id; - return node.id; -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/ExternalInputConnectionsList.tsx b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/ExternalInputConnectionsList.tsx deleted file mode 100644 index 28c13d4043..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/ExternalInputConnectionsList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { type Node } from "@xyflow/react"; - -import { InfoBox } from "@/components/shared/InfoBox"; -import { Paragraph } from "@/components/ui/typography"; - -import { NodeListItem } from "./NodeListItem"; - -interface ExternalInputConnectionsListProps { - nodes: Node[]; - excludedNodeIds?: Set; - onExcludeNode?: (nodeId: string) => void; - onIncludeNode?: (nodeId: string) => void; -} - -export function ExternalInputConnectionsList({ - nodes, - excludedNodeIds, - onExcludeNode, - onIncludeNode, -}: ExternalInputConnectionsListProps) { - if (nodes.length === 0) { - return null; - } - - return ( - - - These inputs are connected to tasks that are not part of the selection. - Exclude them to keep their external connections intact. - - - {nodes.map((node) => ( - - ))} - - - ); -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NewSubgraphDialog.tsx b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NewSubgraphDialog.tsx deleted file mode 100644 index 19d0beb722..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NewSubgraphDialog.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import type { Node } from "@xyflow/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { InputDialog } from "@/components/shared/Dialogs/InputDialog"; -import { InfoBox } from "@/components/shared/InfoBox"; -import { BlockStack } from "@/components/ui/layout"; -import { - type ComponentSpec, - isGraphImplementation, -} from "@/utils/componentSpec"; -import { getUniqueTaskName, validateTaskName } from "@/utils/unique"; - -import { checkExternalInputConnections } from "./checkExternalInputConnections"; -import { checkForOrphanedNodes } from "./checkForOrphanedNodes"; -import { ExternalInputConnectionsList } from "./ExternalInputConnectionsList"; -import { NodesList } from "./NodesList"; -import { OrphanedNodeList } from "./OrphanedNodeList"; -import { canGroupNodes } from "./utils"; - -interface NewSubgraphDialogProps { - open: boolean; - selectedNodes: Node[]; - currentSubgraphSpec: ComponentSpec; - onClose: () => void; - onCreateSubgraph: (filteredNodes: Node[], name: string) => void; -} - -const DEFAULT_SUBGRAPH_NAME = "New Subgraph"; - -export const NewSubgraphDialog = ({ - open, - selectedNodes, - currentSubgraphSpec, - onClose, - onCreateSubgraph, -}: NewSubgraphDialogProps) => { - const [excludedNodeIds, setExcludedNodeIds] = useState>( - new Set(), - ); - const [defaultName, setDefaultName] = useState(""); - - const excludeNode = useCallback((nodeId: string) => { - setExcludedNodeIds((prev) => new Set(prev).add(nodeId)); - }, []); - - const includeNode = useCallback((nodeId: string) => { - setExcludedNodeIds((prev) => { - const newSet = new Set(prev); - newSet.delete(nodeId); - return newSet; - }); - }, []); - - const activeNodes = selectedNodes.filter( - (node) => !excludedNodeIds.has(node.id), - ); - - // Orphans in the original selection - const orphanedNodes = useMemo( - () => checkForOrphanedNodes(selectedNodes, currentSubgraphSpec), - [selectedNodes, currentSubgraphSpec], - ); - - // Orphans in the current active selection (after user exclusions) - const activeOrphanedNodes = useMemo( - () => checkForOrphanedNodes(activeNodes, currentSubgraphSpec), - [activeNodes, currentSubgraphSpec], - ); - - // Inputs with external connections in the current active selection (after user exclusions) - const inputsWithExternalConnections = useMemo( - () => checkExternalInputConnections(activeNodes, currentSubgraphSpec), - [activeNodes, currentSubgraphSpec], - ); - - const hasActiveOrphans = activeOrphanedNodes.length > 0; - - const { canGroup, errorMessage } = canGroupNodes(activeNodes); - - const dialogContent = useMemo(() => { - const allOrphans = orphanedNodes - .concat(activeOrphanedNodes) - .filter( - (node, index, self) => - index === self.findIndex((n) => n.id === node.id), - ); - - const allOrphanedNodeIds = new Set(allOrphans.map((node) => node.id)); - - return ( - - - - {canGroup && hasActiveOrphans && ( - - )} - - {canGroup && inputsWithExternalConnections.length > 0 && ( - - )} - - {!canGroup && errorMessage && ( - - {errorMessage} - - )} - - ); - }, [ - selectedNodes, - activeNodes, - orphanedNodes, - activeOrphanedNodes, - inputsWithExternalConnections, - hasActiveOrphans, - canGroup, - errorMessage, - excludedNodeIds, - excludeNode, - includeNode, - ]); - - const handleConfirm = useCallback( - (name: string) => { - const submittedNodes = selectedNodes.filter( - (node) => !excludedNodeIds.has(node.id), - ); - onCreateSubgraph(submittedNodes, name.trim()); - onClose(); - }, - [selectedNodes, excludedNodeIds, onCreateSubgraph, onClose], - ); - - const handleCancel = useCallback(() => { - onClose(); - }, [onClose]); - - const validate = useCallback( - (value: string) => { - if (isGraphImplementation(currentSubgraphSpec.implementation)) { - return validateTaskName( - value.trim(), - currentSubgraphSpec.implementation.graph, - true, - ); - } - return null; - }, - [currentSubgraphSpec], - ); - - useEffect(() => { - if (open) { - setExcludedNodeIds(new Set()); - - let name = DEFAULT_SUBGRAPH_NAME; - if (isGraphImplementation(currentSubgraphSpec.implementation)) { - name = getUniqueTaskName( - currentSubgraphSpec.implementation.graph, - DEFAULT_SUBGRAPH_NAME, - ); - } - setDefaultName(name); - } - }, [open, currentSubgraphSpec]); - - const disabled = hasActiveOrphans || !canGroup; - - return ( - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodeListItem.tsx b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodeListItem.tsx deleted file mode 100644 index 5159bf9ca3..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodeListItem.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { type Node } from "@xyflow/react"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { InlineStack } from "@/components/ui/layout"; -import { Paragraph } from "@/components/ui/typography"; -import { cn } from "@/lib/utils"; - -import { isFlexNode } from "../../types"; -import { getNodeTypeColor } from "./utils"; - -interface NodeListItemProps { - node: Node; - excludedNodeIds?: Set; - isOrphaned?: boolean; - onExcludeNode?: (nodeId: string) => void; - onIncludeNode?: (nodeId: string) => void; -} - -export function NodeListItem({ - node, - excludedNodeIds, - isOrphaned, - onExcludeNode, - onIncludeNode, -}: NodeListItemProps) { - const isExcluded = excludedNodeIds?.has(node.id); - - const handleButtonClick = () => { - if (onExcludeNode && !isExcluded) { - onExcludeNode(node.id); - } else if (onIncludeNode && isExcluded) { - onIncludeNode(node.id); - } - }; - - const isFlexType = isFlexNode(node); - - const displayValue = isFlexType - ? `Sticky Note: ${node.data.properties.title.length > 0 ? node.data.properties.title : node.id}` - : node.id; - - return ( - - - - - {displayValue} - - {isOrphaned && ( - - - - Orphaned - - - )} - - - {isExcluded ? : } - - - ); -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodesList.tsx b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodesList.tsx deleted file mode 100644 index 43309234dd..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/NodesList.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { type Node } from "@xyflow/react"; -import { ChevronRight } from "lucide-react"; -import { useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; - -import { NodeListItem } from "./NodeListItem"; - -interface NodesListProps { - nodes: Node[]; - title?: string; - excludedNodeIds?: Set; - orphanedNodeIds?: Set; - onExcludeNode?: (nodeId: string) => void; - onIncludeNode?: (nodeId: string) => void; -} - -export function NodesList({ - nodes, - title = `View nodes (${nodes.length})`, - excludedNodeIds, - orphanedNodeIds, - onExcludeNode, - onIncludeNode, -}: NodesListProps) { - const [isOpen, setIsOpen] = useState(false); - - if (nodes.length === 0) { - return null; - } - - return ( - - - - - {title} - - - - - - {nodes.map((node) => ( - - ))} - - - - - ); -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/OrphanedNodeList.tsx b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/OrphanedNodeList.tsx deleted file mode 100644 index 31593a16b7..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/OrphanedNodeList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { type Node } from "@xyflow/react"; - -import { InfoBox } from "@/components/shared/InfoBox"; -import { Paragraph } from "@/components/ui/typography"; - -import { NodeListItem } from "./NodeListItem"; - -interface OrphanedNodeListProps { - nodes: Node[]; - excludedNodeIds?: Set; - onExcludeNode?: (nodeId: string) => void; - onIncludeNode?: (nodeId: string) => void; -} - -export function OrphanedNodeList({ - nodes, - excludedNodeIds, - onExcludeNode, - onIncludeNode, -}: OrphanedNodeListProps) { - if (nodes.length === 0) { - return null; - } - - return ( - - - These nodes are not connected to anything else in the selection. Exclude - them to create a valid subgraph. - - - {nodes.map((node) => ( - - ))} - - - ); -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/utils.ts deleted file mode 100644 index 8773192c40..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/create/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Node } from "@xyflow/react"; - -interface GroupingValidation { - canGroup: boolean; - errorMessage?: string; -} - -export const canGroupNodes = (nodes: Node[]): GroupingValidation => { - if (nodes.length <= 1) { - return { - canGroup: false, - errorMessage: "At least 2 nodes are required to create a subgraph.", - }; - } - - if (nodes.filter((node) => node.type === "task").length === 0) { - return { - canGroup: false, - errorMessage: "At least 1 task node is required to create a subgraph.", - }; - } - - return { - canGroup: true, - }; -}; - -export const getNodeTypeColor = (nodeType: string | undefined): string => { - switch (nodeType) { - case "flex": - return "bg-yellow-300"; - case "input": - return "bg-blue-500"; - case "output": - return "bg-violet-500"; - case "task": - default: - return "bg-gray-500"; - } -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/view/SubgraphBreadcrumbs.tsx b/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/view/SubgraphBreadcrumbs.tsx deleted file mode 100644 index 84615c02d2..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/Subgraphs/view/SubgraphBreadcrumbs.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useNavigate } from "@tanstack/react-router"; -import { useCallback } from "react"; - -import { SubgraphBreadcrumbsView } from "@/components/shared/SubgraphBreadcrumbsView"; -import { buildExecutionUrl } from "@/hooks/useSubgraphBreadcrumbs"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; - -export const SubgraphBreadcrumbs = () => { - const navigate = useNavigate(); - const { currentSubgraphPath, navigateToPath } = useComponentSpec(); - const executionData = useExecutionDataOptional(); - const runId = executionData?.runId; - const segments = executionData?.segments || []; - - const getExecutionIdForIndex = useCallback( - (targetIndex: number): string | undefined => { - if (!runId) return undefined; - - if (targetIndex === 0) { - return runId; - } - - const segmentIndex = targetIndex - 1; - if (segmentIndex >= 0 && segmentIndex < segments.length) { - return segments[segmentIndex].executionId; - } - - return undefined; - }, - [runId, segments], - ); - - const handleBreadcrumbClick = useCallback( - (targetIndex: number) => { - const targetPath = currentSubgraphPath.slice(0, targetIndex + 1); - - navigateToPath(targetPath); - - if (runId && executionData) { - const targetExecutionId = getExecutionIdForIndex(targetIndex); - const url = buildExecutionUrl(runId, targetExecutionId); - navigate({ to: url }); - } - }, - [ - currentSubgraphPath, - navigateToPath, - runId, - executionData, - getExecutionIdForIndex, - navigate, - ], - ); - - return ( - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsEditor.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsEditor.tsx deleted file mode 100644 index c804332ffb..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsEditor.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Heading, Paragraph } from "@/components/ui/typography"; -import type { AnnotationConfig, Annotations } from "@/types/annotations"; -import { - DEFAULT_COMMON_ANNOTATIONS, - getAnnotationValue, -} from "@/utils/annotations"; - -import { AnnotationsInput } from "./AnnotationsInput"; -import { - NewAnnotationRow, - type NewAnnotationRowData, -} from "./NewAnnotationRow"; - -interface AnnotationsEditorProps { - annotations: Annotations; - pinnedAnnotations?: AnnotationConfig[]; - onSave: (key: string, value: string) => void; - onRemove: (key: string) => void; - newRows: Array; - onNewRowBlur: (newRow: NewAnnotationRowData) => void; - onRemoveNewRow: (newRow: NewAnnotationRowData) => void; - onAddNewRow: () => void; -} - -export const AnnotationsEditor = ({ - annotations, - pinnedAnnotations = DEFAULT_COMMON_ANNOTATIONS, - onSave, - onRemove, - newRows, - onNewRowBlur, - onRemoveNewRow, - onAddNewRow, -}: AnnotationsEditorProps) => { - const remainingAnnotations = Object.entries(annotations).filter( - ([key]) => !pinnedAnnotations.some((config) => config.annotation === key), - ); - - return ( - - - Annotations - - - - New - - - - {pinnedAnnotations.map((config) => ( - - - - {config.label} {config.unit && `(${config.unit})`} - - {config.required && ( - - * - - )} - - - onSave(config.annotation, newValue)} - annotations={annotations} - config={config} - /> - - ))} - - {remainingAnnotations.map(([key]) => ( - - - {key} - - - onSave(key, newValue)} - onDelete={() => onRemove(key)} - annotations={annotations} - deletable - /> - - ))} - - {newRows.map((row, idx) => ( - - ))} - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsSection.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsSection.tsx deleted file mode 100644 index 91ff53dd23..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsSection.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { BlockStack } from "@/components/ui/layout"; -import { Separator } from "@/components/ui/separator"; -import useToastNotification from "@/hooks/useToastNotification"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import type { AnnotationConfig, Annotations } from "@/types/annotations"; -import { getAnnotationValue, HIDDEN_ANNOTATIONS } from "@/utils/annotations"; -import type { TaskSpec } from "@/utils/componentSpec"; - -import { AnnotationsEditor } from "./AnnotationsEditor"; -import { ComputeResourcesEditor } from "./ComputeResourcesEditor"; -import type { NewAnnotationRowData } from "./NewAnnotationRow"; -import { - getCloudProviderConfig, - getCommonAnnotations, - getProviderSchema, - launcherTaskAnnotationSchema, - parseSchemaToAnnotationConfig, -} from "./utils"; - -interface AnnotationsSectionProps { - taskSpec: TaskSpec; - onApply: (annotations: Annotations) => void; -} - -export const AnnotationsSection = ({ - taskSpec, - onApply, -}: AnnotationsSectionProps) => { - const notify = useToastNotification(); - const { track } = useAnalytics(); - - const rawAnnotations = taskSpec.annotations ?? {}; - - const [annotations, setAnnotations] = useState({ - ...rawAnnotations, - }); - - const [cloudProviderConfig, setCloudProviderConfig] = - useState(null); - const [pinnedAnnotations, setPinnedAnnotations] = useState< - AnnotationConfig[] - >([]); - const [computeResources, setComputeResources] = useState( - [], - ); - const [previousProvider, setPreviousProvider] = useState( - undefined, - ); - - // Track new rows separately until they have a key - const [newRows, setNewRows] = useState>([]); - - const selectedProvider = cloudProviderConfig - ? getAnnotationValue(annotations, cloudProviderConfig.annotation) - : undefined; - - const commonAnnotations = useMemo(() => { - const managedAnnotationKeys = new Set([ - ...computeResources.map((r) => r.annotation), - ...(cloudProviderConfig ? [cloudProviderConfig.annotation] : []), - ]); - - return Object.entries(annotations).reduce( - (acc, [key, value]) => { - if (!managedAnnotationKeys.has(key) && !HIDDEN_ANNOTATIONS.has(key)) { - acc[key] = value; - } - return acc; - }, - {}, - ); - }, [annotations, computeResources, cloudProviderConfig]); - - const handleAddNewRow = useCallback(() => { - const newRow = { id: Date.now().toString(), key: "", value: "" }; - setNewRows((rows) => [...rows, newRow]); - }, []); - - const handleRemoveNewRow = useCallback((newRow: NewAnnotationRowData) => { - setNewRows((rows) => rows.filter((row) => row.id !== newRow.id)); - }, []); - - const handleNewRowBlur = useCallback( - (newRow: NewAnnotationRowData) => { - if (newRow.key.trim() && !(newRow.key in annotations)) { - track("pipeline_editor.task_node.annotation_added"); - const newAnnotations = { - ...annotations, - [newRow.key]: newRow.value, - }; - setAnnotations(newAnnotations); - onApply(newAnnotations); - - setNewRows((rows) => rows.filter((row) => row.id !== newRow.id)); - } else { - if (newRow.key.trim() && newRow.key in annotations) { - notify("Annotation key already exists", "warning"); - } - - setNewRows((rows) => - rows.map((row) => - row.id === newRow.id ? { ...row, ...newRow } : row, - ), - ); - } - }, - [annotations, onApply, notify], - ); - - const handleRemove = useCallback( - (key: string) => { - const { [key]: _, ...rest } = annotations; - const newAnnotations = rest; - setAnnotations(newAnnotations); - onApply(newAnnotations); - }, - [annotations, onApply], - ); - - const handleSave = useCallback( - (key: string, value: string | undefined) => { - if (value === undefined || value === "") { - // If value is empty or undefined, remove the annotation - handleRemove(key); - return; - } - - const newAnnotations = { - ...annotations, - [key]: value, - }; - - setAnnotations(newAnnotations); - onApply(newAnnotations); - }, - [annotations, onApply, handleRemove], - ); - - useEffect(() => { - setAnnotations(rawAnnotations); - }, [rawAnnotations]); - - useEffect(() => { - try { - const providerConfig = getCloudProviderConfig( - launcherTaskAnnotationSchema, - ); - setCloudProviderConfig(providerConfig); - - const common = getCommonAnnotations(launcherTaskAnnotationSchema); - setPinnedAnnotations(common); - } catch (error) { - console.error("Failed to load launcher annotation schema:", error); - } - }, []); - - // Provider-specific compute resources - useEffect(() => { - if (selectedProvider !== previousProvider) { - try { - // Get previous provider's annotations to remove - let annotationsToRemove: string[] = []; - if (previousProvider) { - const previousProviderSchema = getProviderSchema( - launcherTaskAnnotationSchema, - previousProvider, - ); - if (previousProviderSchema) { - const previousResources = parseSchemaToAnnotationConfig( - previousProviderSchema, - ); - annotationsToRemove = previousResources.map((r) => r.annotation); - } - } - - // Get new provider's schema - let newResources: AnnotationConfig[] = []; - if (selectedProvider) { - const providerSchema = getProviderSchema( - launcherTaskAnnotationSchema, - selectedProvider, - ); - - if (providerSchema) { - const parsedResources = - parseSchemaToAnnotationConfig(providerSchema); - newResources = parsedResources.filter((res) => !res.hidden); - } - } - - setComputeResources(newResources); - - // Remove old provider annotations if switching providers - if (annotationsToRemove.length > 0) { - const cleanedAnnotations = { ...annotations }; - annotationsToRemove.forEach((key) => { - delete cleanedAnnotations[key]; - }); - setAnnotations(cleanedAnnotations); - onApply(cleanedAnnotations); - } - - setPreviousProvider(selectedProvider); - } catch (error) { - console.error("Failed to load provider schema:", error); - setComputeResources([]); - } - } - }, [selectedProvider, previousProvider, annotations, onApply]); - - return ( - - - - - - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/NewAnnotationRow.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/NewAnnotationRow.tsx deleted file mode 100644 index 20f5f00f17..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/NewAnnotationRow.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { type FocusEvent, useCallback, useMemo, useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { Input } from "@/components/ui/input"; -import { InlineStack } from "@/components/ui/layout"; - -export type NewAnnotationRowData = { - id: string; - key: string; - value: string; -}; - -interface NewAnnotationRowProps { - row: NewAnnotationRowData; - autofocus: boolean; - onBlur: (row: NewAnnotationRowData) => void; - onRemove: (row: NewAnnotationRowData) => void; -} - -export const NewAnnotationRow = ({ - row, - autofocus, - onBlur, - onRemove, -}: NewAnnotationRowProps) => { - const [key, setKey] = useState(row.key); - const [value, setValue] = useState(row.value); - - const newRow = useMemo(() => ({ ...row, key, value }), [row, key, value]); - - const handleRowBlur = useCallback( - (e: FocusEvent) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - onBlur(newRow); - } - }, - [newRow, onBlur], - ); - - const handleRowRemove = useCallback(() => { - onRemove(newRow); - }, [newRow, onRemove]); - - return ( - - - setKey(e.target.value)} - className="w-39 ml-1" - autoFocus={autofocus} - /> - setValue(e.target.value)} - className="flex-1" - /> - - - - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputDialog.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputDialog.tsx deleted file mode 100644 index 34de2703fe..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputDialog.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { MultilineTextInputDialog } from "@/components/shared/Dialogs/MultilineTextInputDialog"; -import type { ArgumentInput } from "@/types/arguments"; - -import { getInputValue, typeSpecToString } from "./utils"; - -export const ArgumentInputDialog = ({ - argument, - placeholder, - lastSubmittedValue, - open, - onCancel, - onConfirm, -}: { - argument: ArgumentInput; - placeholder?: string; - lastSubmittedValue?: string; - open: boolean; - onCancel: () => void; - onConfirm: (value: string) => void; -}) => { - const titleMarkup = ( - <> - {argument.key}{" "} - - ({typeSpecToString(argument.inputSpec.type)} - {!argument.inputSpec.optional ? "*" : ""}) - - > - ); - - const description = - argument.inputSpec.description || "Enter the value for this argument."; - const initialValue = getArgumentDisplayValue(argument, lastSubmittedValue); - - return ( - - ); -}; - -function getArgumentDisplayValue( - argument: ArgumentInput, - lastSubmittedValue?: string, -): string { - if (argument.isRemoved) { - return lastSubmittedValue || getInputValue(argument) || ""; - } - return getInputValue(argument) || ""; -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx deleted file mode 100644 index 92ad337319..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx +++ /dev/null @@ -1,566 +0,0 @@ -import equal from "fast-deep-equal"; -import { - type ChangeEvent, - type MouseEvent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; - -import TooltipButton from "@/components/shared/Buttons/TooltipButton"; -import { DescriptionWithLinks } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/DescriptionWithLinks"; -import { SelectSecretDialog } from "@/components/shared/SecretsManagement/SelectSecretDialog"; -import { createSecretArgument } from "@/components/shared/SecretsManagement/types"; -import { Icon } from "@/components/ui/icon"; -import { Input } from "@/components/ui/input"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Paragraph } from "@/components/ui/typography"; -import { useCallbackOnUnmount } from "@/hooks/useCallbackOnUnmount"; -import useToastNotification from "@/hooks/useToastNotification"; -import { cn } from "@/lib/utils"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import type { ArgumentInput } from "@/types/arguments"; -import { - isDynamicDataArgument, - isGraphImplementation, -} from "@/utils/componentSpec"; -import { tracking } from "@/utils/tracking"; - -import { ArgumentInputDialog } from "./ArgumentInputDialog"; -import { DynamicDataArgumentInput } from "./DynamicDataArgumentInput"; -import { DynamicDataDropdown } from "./DynamicDataDropdown"; -import { - createSystemDataArgument, - getDynamicDataDisplayInfo, - type TaskAnnotations, -} from "./dynamicDataUtils"; -import { - getDefaultValue, - getInputValue, - getInputValueAsString, - getPlaceholder, - typeSpecToString, -} from "./utils"; - -interface PlainArgumentInputProps { - argument: ArgumentInput; - inputValue: string; - placeholder: string; - disabled: boolean; - disabledCopy: boolean; - disabledReset: boolean; - isDynamicDataEnabled: boolean; - isTaskLevel: boolean; - taskAnnotations?: TaskAnnotations; - onInputChange: (e: ChangeEvent) => void; - onBlur: () => void; - onExpand: () => void; - onCopy: () => void; - onReset: () => void; - onRemove: () => void; - onOpenSecretDialog: () => void; - onSelectSystemData: (key: string) => void; -} - -const ACTIONS_BASE_CLASS = - "hover:bg-transparent hover:text-blue-500 hidden group-hover:flex"; - -const PlainArgumentInput = ({ - argument, - inputValue, - placeholder, - disabled, - disabledCopy, - disabledReset, - isDynamicDataEnabled, - isTaskLevel, - taskAnnotations, - onInputChange, - onBlur, - onExpand, - onCopy, - onReset, - onRemove, - onOpenSecretDialog, - onSelectSystemData, -}: PlainArgumentInputProps) => { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - return ( - <> - - - - - {placeholder && !inputValue && ( - {placeholder} - )} - - - - {isDynamicDataEnabled && ( - - )} - - - - {!disabledCopy && ( - - - - )} - {!disabledReset && ( - - - - )} - - {argument.isRemoved ? ( - - ) : ( - - )} - - - > - ); -}; - -export const ArgumentInputField = ({ - argument, - disabled = false, - taskAnnotations, - onSave, -}: { - argument: ArgumentInput; - disabled?: boolean; - taskAnnotations?: TaskAnnotations; - onSave: (argument: ArgumentInput) => void; -}) => { - const notify = useToastNotification(); - const { track } = useAnalytics(); - const { currentSubgraphSpec } = useComponentSpec(); - - const [inputValue, setInputValue] = useState(getInputValue(argument) ?? ""); - const [lastSubmittedValue, setLastSubmittedValue] = useState( - getInputValue(argument) ?? "", - ); - - const [isTextareaDialogOpen, setIsTextareaDialogOpen] = useState(false); - const [isSelectSecretDialogOpen, setIsSelectSecretDialogOpen] = - useState(false); - - const undoValue = useMemo(() => argument, []); - - // Multi-node data is available when editing task arguments (always true in TaskNode context) - // The "pipeline level" restriction only applies to pipeline input values, not task arguments - const isTaskLevel = true; - - const isDynamicData = useMemo( - () => isDynamicDataArgument(argument.value), - [argument.value], - ); - - const dynamicDataDisplayInfo = useMemo(() => { - if (!isDynamicDataArgument(argument.value)) return null; - return getDynamicDataDisplayInfo(argument.value.dynamicData); - }, [argument.value]); - - const hint = argument.inputSpec.annotations?.hint as string | undefined; - - const handleInputChange = (e: ChangeEvent) => { - const value = (e.target as HTMLInputElement).value; - setInputValue(value); - }; - - const handleBlur = () => { - const value = getInputValueAsString(inputValue); - handleSubmit(value); - }; - - const handleSubmit = useCallback( - (value: string) => { - if (value === lastSubmittedValue) return; - - const updatedArgument = { - ...argument, - value, - isRemoved: false, - }; - - onSave(updatedArgument); - setLastSubmittedValue(value); - }, - [inputValue, lastSubmittedValue, argument, onSave], - ); - - const handleRemove = () => { - const updatedArgument = { - ...argument, - value: getInputValueAsString(inputValue), - isRemoved: !argument.isRemoved, - }; - - if (!updatedArgument.isRemoved && updatedArgument.value === "") { - const defaultValue = getDefaultValue(updatedArgument); - - updatedArgument.value = defaultValue; - - setInputValue(defaultValue); - } - - onSave(updatedArgument); - }; - - const handleReset = () => { - const defaultValue = getDefaultValue(argument); - - setInputValue(defaultValue); - onSave({ ...argument, value: defaultValue }); - }; - - const handleUndo = (e: MouseEvent) => { - e.stopPropagation(); - - if (disabled) return; - - setInputValue(getInputValue(undoValue) ?? ""); - onSave({ ...undoValue }); - }; - - const handleExpand = useCallback(() => { - if (disabled) return; - setIsTextareaDialogOpen(true); - }, [disabled]); - - const handleDialogConfirm = useCallback( - (value: string) => { - setInputValue(value); - setIsTextareaDialogOpen(false); - - handleSubmit(value); - }, - [handleSubmit], - ); - - const handleDialogCancel = useCallback(() => { - setIsTextareaDialogOpen(false); - }, []); - - const handleOpenSecretDialog = useCallback(() => { - if (disabled) return; - track("pipeline_editor.task_node.input_action.click", { - action: "use_dynamic_data", - }); - track("pipeline_editor.task_node.select_secret_impression"); - setIsSelectSecretDialogOpen(true); - }, [disabled, track]); - - const handleSecretSelect = useCallback( - (selectedSecretName: string) => { - const secretArg = createSecretArgument(selectedSecretName); - setInputValue(""); - setIsSelectSecretDialogOpen(false); - onSave({ ...argument, value: secretArg, isRemoved: false }); - }, - [argument, onSave], - ); - - const handleSelectSystemData = useCallback( - (key: string) => { - const systemDataArg = createSystemDataArgument(key); - setInputValue(""); - onSave({ ...argument, value: systemDataArg, isRemoved: false }); - }, - [argument, onSave], - ); - - const handleClearDynamicData = useCallback(() => { - if (disabled) return; - setInputValue(""); - onSave({ ...argument, value: "", isRemoved: false }); - }, [disabled, argument, onSave]); - - const handleCopy = useCallback(() => { - if (disabled || argument.isRemoved) return; - - void navigator.clipboard - .writeText(inputValue) - .then(() => { - notify( - `Argument "${argument.inputSpec.name}" copied to clipboard`, - "success", - ); - }) - .catch((err) => { - notify( - `Failed to copy text: ${err instanceof Error ? err.message : String(err)}`, - "error", - ); - }); - }, [inputValue, disabled, argument]); - - const canUndo = useMemo( - () => !equal(argument, undoValue), - [argument, undoValue], - ); - - const placeholder = useMemo(() => { - const graphSpec = isGraphImplementation(currentSubgraphSpec.implementation) - ? currentSubgraphSpec.implementation.graph - : undefined; - const inputPlaceholder = getPlaceholder(argument.value, graphSpec); - - if (inputPlaceholder) { - return inputPlaceholder; - } - - if (argument.inputSpec.default !== undefined) { - return argument.inputSpec.default; - } - - if (argument.isRemoved) { - return ""; - } - - return ""; - }, [argument, currentSubgraphSpec]); - - useEffect(() => { - const value = getInputValue(argument); - if (value !== undefined && value !== inputValue) { - setInputValue(value); - setLastSubmittedValue(value); - } - }, [argument]); - - const disabledCopy = useMemo( - () => disabled || argument.isRemoved || inputValue === "", - [inputValue, argument.isRemoved, disabled], - ); - - const disabledReset = useMemo( - () => - disabled || - argument.isRemoved || - argument.value === getDefaultValue(argument), - [argument, disabled], - ); - - useCallbackOnUnmount(handleBlur); - - return ( - <> - - - - - {argument.inputSpec.name.replace(/_/g, " ")} - - - - ({typeSpecToString(argument.inputSpec.type)} - {!argument.inputSpec.optional ? "*" : ""}) - - - - {argument.inputSpec.description && ( - - - - - - - - - - - - - )} - - {!!hint && ( - - - - - {hint} - - )} - - - - - - Recently changed - - - - - {isDynamicData && dynamicDataDisplayInfo ? ( - - ) : ( - - )} - - - - - - - > - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditor.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditor.tsx deleted file mode 100644 index 0dd9a201fb..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsEditor.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useMemo } from "react"; - -import { BlockStack } from "@/components/ui/layout"; -import { Heading } from "@/components/ui/typography"; -import type { ArgumentInput } from "@/types/arguments"; -import type { ArgumentType, TaskSpec } from "@/utils/componentSpec"; - -import { ArgumentInputField } from "./ArgumentInputField"; -import type { TaskAnnotations } from "./dynamicDataUtils"; -import { getArgumentInputs } from "./utils"; - -interface ArgumentsEditorProps { - taskSpec: TaskSpec; - setArguments: (args: Record) => void; - disabled?: boolean; -} - -export const ArgumentsEditor = ({ - taskSpec, - setArguments, - disabled = false, -}: ArgumentsEditorProps) => { - const argumentInputs = getArgumentInputs(taskSpec); - const taskAnnotations = useMemo( - () => taskSpec.annotations as TaskAnnotations | undefined, - [taskSpec.annotations], - ); - - const handleArgumentSave = (argument: ArgumentInput) => { - const argumentValues = { - ...Object.fromEntries( - argumentInputs - .filter(({ isRemoved }) => !isRemoved) - .map(({ key, value }) => [key, value]), - ), - }; - - if (argument.isRemoved) { - delete argumentValues[argument.key]; - } else { - argumentValues[argument.key] = argument.value; - } - - setArguments(argumentValues); - }; - - return ( - - Inputs - - {argumentInputs.map((argument) => { - // Aggregator inputs (agg_* and output_type) are read-only - // - agg_* inputs are set by connections - // - output_type is set by the dropdown selector on the node - const isAggregatorInput = - argument.key.startsWith("agg_") || argument.key === "output_type"; - - return ( - - ); - })} - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsSection.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsSection.tsx deleted file mode 100644 index 93875bbb58..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentsSection.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Paragraph } from "@/components/ui/typography"; -import type { ArgumentType, TaskSpec } from "@/utils/componentSpec"; - -import { ArgumentsEditor } from "../ArgumentsEditor"; - -interface ArgumentsSectionProps { - taskSpec: TaskSpec; - setArguments: (args: Record) => void; - disabled?: boolean; -} - -const ArgumentsSection = ({ - taskSpec, - setArguments, - disabled = false, -}: ArgumentsSectionProps) => { - return ( - - - Configure the arguments for this task node. - - - - ); -}; - -export default ArgumentsSection; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/DynamicDataDropdown.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/DynamicDataDropdown.tsx deleted file mode 100644 index b8be0efafb..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/DynamicDataDropdown.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Icon } from "@/components/ui/icon"; -import { InlineStack } from "@/components/ui/layout"; -import { cn } from "@/lib/utils"; - -import { getDynamicDataGroups, type TaskAnnotations } from "./dynamicDataUtils"; - -interface DynamicDataDropdownProps { - disabled: boolean; - isTaskLevel: boolean; - taskAnnotations?: TaskAnnotations; - triggerClassName?: string; - onOpenSecretDialog: () => void; - onSelectSystemData: (key: string) => void; - onOpenChange?: (open: boolean) => void; -} - -export const DynamicDataDropdown = ({ - disabled, - isTaskLevel, - taskAnnotations, - triggerClassName, - onOpenSecretDialog, - onSelectSystemData, - onOpenChange, -}: DynamicDataDropdownProps) => { - const [isOpen, setIsOpen] = useState(false); - - const dynamicDataGroups = getDynamicDataGroups(isTaskLevel, taskAnnotations); - - const isSecretsOnly = - dynamicDataGroups.length === 1 && dynamicDataGroups[0].requiresDialog; - - const handleOpenChange = (open: boolean) => { - setIsOpen(open); - onOpenChange?.(open); - }; - - const handleSecretDialogOpen = () => { - setIsOpen(false); - onOpenChange?.(false); - onOpenSecretDialog(); - }; - - if (isSecretsOnly) { - return ( - - - - ); - } - - return ( - - - - - - - - {dynamicDataGroups.map((group, index) => ( - - {index > 0 && } - - - - {group.title} - - - {group.requiresDialog ? ( - - - Select Secret... - - ) : ( - group.options.map((option) => ( - onSelectSystemData(option.key)} - > - - {option.title} - - )) - )} - - ))} - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/index.ts b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/index.ts deleted file mode 100644 index 13adc872ff..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ArgumentInputField"; -export * from "./ArgumentsEditor"; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts index b5645fd63d..33ff5fd849 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils.ts @@ -1,49 +1,4 @@ -import type { ArgumentInput } from "@/types/arguments"; -import { - type ArgumentType, - type GraphSpec, - isSecretArgument, - type TaskSpec, - type TypeSpecType, -} from "@/utils/componentSpec"; -import { isScalar } from "@/utils/types"; - -import { getDisplayValue } from "../TaskNodeCard/handleUtils"; - -export const getArgumentInputs = (taskSpec: TaskSpec) => { - const componentSpec = taskSpec.componentRef.spec; - - const argumentInputs = - componentSpec?.inputs?.map((input) => { - const existingArgument = taskSpec.arguments?.[input.name]; - const initialValue = existingArgument ?? input.default; - - /* - * Some notes on the logic of the ArgumentInput: - * [key] - This is used internally by React to keep track of the Input Component. Must be unique. - * [value] - This is the value of the argument. It cannot be undefined due to React's rules around controlled components. - * [initialValue] - This is the initial value of the argument when the Editor is opened. Immutable. It is used to determine if the argument has been changed during the current editing session. - * [inputSpec] - These are some general constants for the argument. Immutable. It is used to display the argument name and type in the UI. - * [isRemoved] - This is used to remove unwanted arguments from the Task Spec, as specified by the user. This is essentially used in place of an "undefined" input, since React requires an empty string for controlled components. - - * Note that "undefined" and "empty string" are treated differently in the task spec, but we can only use "empty string" in the UI due to React's rules around controlled components. - * The difference is best seen in a required argument with a linked node: - * - The connection will disappear and the connection point turn red when the node is removed, since it is required. This is what the "Remove" button is for. - * - An empty string is a valid input and will result in the connection being severed, but the connection point will not turn red. This is what the "Reset to Default" button is for. - * - A severed connection cannot be reconnected in the argument editor once applied and must be redrawn on the graph. - */ - - return { - key: input.name, - value: initialValue ?? "", - initialValue: initialValue ?? "", - inputSpec: input, - isRemoved: existingArgument === undefined, - } as ArgumentInput; - }) ?? []; - - return argumentInputs; -}; +import type { TypeSpecType } from "@/utils/componentSpec"; export const typeSpecToString = (typeSpec?: TypeSpecType): string => { if (typeSpec === undefined) { @@ -54,43 +9,3 @@ export const typeSpecToString = (typeSpec?: TypeSpecType): string => { } return JSON.stringify(typeSpec); }; - -export const getPlaceholder = ( - argument: ArgumentType, - graphSpec?: GraphSpec, -) => { - if (isScalar(argument) || !argument) { - return null; - } - - return getDisplayValue(argument, graphSpec); -}; - -export const getInputValue = (argumentInput: ArgumentInput) => { - const argument = argumentInput.value; - - if (argument === undefined) { - return argumentInput.inputSpec.default; - } - - if (isSecretArgument(argument)) { - return ""; - } - - if (isScalar(argument)) { - return argument; - } - - return ""; -}; - -export const getInputValueAsString = (value: string) => { - if (isScalar(value)) { - return value.toString().trim(); - } - return ""; -}; - -export const getDefaultValue = (argumentInput: ArgumentInput) => { - return argumentInput.inputSpec.default ?? ""; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx deleted file mode 100644 index ee2d3cadd6..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { type NodeProps } from "@xyflow/react"; -import { memo, useMemo } from "react"; - -import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useEdgeSelectionHighlight } from "@/hooks/useEdgeSelectionHighlight"; -import { cn } from "@/lib/utils"; -import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; -import { TaskNodeProvider } from "@/providers/TaskNodeProvider"; -import type { TaskNodeData } from "@/types/taskNode"; -import { isCacheDisabled } from "@/utils/cache"; - -import { StatusIndicator } from "./StatusIndicator"; -import { TaskNodeCard } from "./TaskNodeCard"; - -const TaskNodeSkeleton = () => ( - - - -); - -const TaskNodeInternal = ({ data, selected, id }: NodeProps) => { - const executionData = useExecutionDataOptional(); - - const typedData = useMemo(() => data as TaskNodeData, [data]); - - const status = useMemo(() => { - const taskId = typedData.taskId ?? ""; - return executionData?.taskExecutionStatusMap.get(taskId); - }, [executionData?.taskExecutionStatusMap, typedData.taskId]); - - const disabledCache = isCacheDisabled(typedData.taskSpec); - - const { isConnectedToSelectedEdge, hasAnySelectedEdge } = - useEdgeSelectionHighlight(id); - - return ( - - - {!!status && ( - - )} - - - - ); -}; - -const TaskNode = withSuspenseWrapper(TaskNodeInternal, TaskNodeSkeleton); - -export default memo(TaskNode); diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/Handles.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/Handles.tsx deleted file mode 100644 index 3ce159c74b..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/Handles.tsx +++ /dev/null @@ -1,430 +0,0 @@ -import { Handle, Position, useConnection } from "@xyflow/react"; -import { - type MouseEvent as ReactMouseEvent, - useCallback, - useMemo, - useState, -} from "react"; -import { useEffect, useRef } from "react"; - -import { getDynamicDataDisplayInfo } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/dynamicDataUtils"; -import { Icon } from "@/components/ui/icon"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { useHandleEdgeSelection } from "@/hooks/useHandleEdgeSelection"; -import { cn } from "@/lib/utils"; -import { useTaskNode } from "@/providers/TaskNodeProvider"; -import { - type ArgumentType, - type InputSpec, - isDynamicDataArgument, - type OutputSpec, -} from "@/utils/componentSpec"; - -type InputHandleProps = { - input: InputSpec; - invalid: boolean; - value?: string; - rawValue?: ArgumentType; - highlight?: boolean; - onLabelClick?: (e: ReactMouseEvent) => void; - onHandleSelectionChange?: (key: string, selected: boolean) => void; -}; - -export const InputHandle = ({ - input, - invalid, - value, - rawValue, - highlight, - onLabelClick, - onHandleSelectionChange, -}: InputHandleProps) => { - const { nodeId, state } = useTaskNode(); - const { selectEdgesForHandle, clearEdgeSelection } = - useHandleEdgeSelection(nodeId); - - const fromHandle = useConnection((connection) => connection.fromHandle?.id); - const toHandle = useConnection((connection) => connection.toHandle?.id); - const fromNode = useConnection((connection) => connection.fromNode?.id); - const toNode = useConnection((connection) => connection.toNode?.id); - - const handleRef = useRef(null); - - const [selected, setSelected] = useState(false); - const [active, setActive] = useState(false); - - const handleId = getInputHandleId(input.name); - - const missing = invalid ? "bg-red-700!" : "bg-gray-500!"; - const hasValue = value !== undefined && value !== null; - const hasDefault = input.default !== undefined && input.default !== ""; - - const dynamicDataInfo = useMemo(() => { - if (!isDynamicDataArgument(rawValue)) return null; - return getDynamicDataDisplayInfo(rawValue.dynamicData); - }, [rawValue]); - - const handleHandleClick = useCallback( - (e: ReactMouseEvent) => { - e.stopPropagation(); - const newSelected = !selected; - setSelected(newSelected); - if (newSelected) { - selectEdgesForHandle(handleId, "target"); - } else { - clearEdgeSelection(); - } - }, - [selected, selectEdgesForHandle, clearEdgeSelection, handleId], - ); - - const handleLabelClick = useCallback( - (e: ReactMouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - onLabelClick?.(e); - setSelected(!selected); - }, - [onLabelClick, selected], - ); - - useEffect(() => { - if ( - (fromHandle === handleId && fromNode === nodeId) || - (toHandle === handleId && toNode === nodeId) - ) { - setActive(true); - } else { - setActive(false); - } - }, [fromHandle, fromNode, toHandle, toNode, handleId, nodeId]); - - useEffect(() => { - if (onHandleSelectionChange) { - const key = input.name; - onHandleSelectionChange(key, selected); - } - }, [input, selected, onHandleSelectionChange]); - - useEffect(() => { - if (!selected) return; - - const handleClickOutside = (e: MouseEvent) => { - const skip = skipHandleDeselect(e); - if (skip) return; - - if (handleRef.current && !handleRef.current.contains(e.target as Node)) { - setSelected(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside, true); - return () => { - document.removeEventListener("mousedown", handleClickOutside, true); - }; - }, [selected]); - - useEffect(() => { - if (!selected) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - setSelected(false); - } - }; - - document.addEventListener("keydown", handleKeyDown, true); - return () => { - document.removeEventListener("keydown", handleKeyDown, true); - }; - }, [selected]); - - return ( - - - - - - - - - {input.name.replace(/_/g, " ")} - - - {(hasValue || hasDefault) && ( - - {dynamicDataInfo && ( - - )} - - - - {hasValue ? value : input.default} - - - - - {dynamicDataInfo && `${dynamicDataInfo.groupTitle}: `} - {hasValue ? value : input.default} - - - - - )} - - - - ); -}; - -type OutputHandleProps = { - output: OutputSpec; - value?: string; - highlight?: boolean; - onLabelClick?: (e: ReactMouseEvent) => void; - onHandleSelectionChange?: (key: string, selected: boolean) => void; -}; - -export const OutputHandle = ({ - output, - value, - highlight, - onLabelClick, - onHandleSelectionChange, -}: OutputHandleProps) => { - const { nodeId, state } = useTaskNode(); - const { selectEdgesForHandle, clearEdgeSelection } = - useHandleEdgeSelection(nodeId); - - const fromHandle = useConnection((connection) => connection.fromHandle?.id); - const toHandle = useConnection((connection) => connection.toHandle?.id); - const fromNode = useConnection((connection) => connection.fromNode?.id); - const toNode = useConnection((connection) => connection.toNode?.id); - - const handleRef = useRef(null); - - const [selected, setSelected] = useState(false); - const [active, setActive] = useState(false); - - const handleId = getOutputHandleId(output.name); - const hasValue = value !== undefined && value !== "" && value !== null; - - const handleHandleClick = useCallback( - (e: ReactMouseEvent) => { - e.stopPropagation(); - const newSelected = !selected; - setSelected(newSelected); - if (newSelected) { - selectEdgesForHandle(handleId, "source"); - } else { - clearEdgeSelection(); - } - }, - [selected, selectEdgesForHandle, clearEdgeSelection, handleId], - ); - - const handleLabelClick = useCallback( - (e: ReactMouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - onLabelClick?.(e); - setSelected(!selected); - }, - [onLabelClick, selected], - ); - - useEffect(() => { - if ( - (fromHandle === handleId && fromNode === nodeId) || - (toHandle === handleId && toNode === nodeId) - ) { - setActive(true); - } else { - setActive(false); - } - }, [fromHandle, fromNode, toHandle, toNode, handleId, nodeId]); - - useEffect(() => { - if (onHandleSelectionChange) { - const key = output.name; - onHandleSelectionChange(key, selected); - } - }, [output, selected, onHandleSelectionChange]); - - useEffect(() => { - if (!selected) return; - - const handleClickOutside = (e: MouseEvent) => { - const skip = skipHandleDeselect(e); - if (skip) return; - - if (handleRef.current && !handleRef.current.contains(e.target as Node)) { - setSelected(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside, true); - return () => { - document.removeEventListener("mousedown", handleClickOutside, true); - }; - }, [selected]); - - useEffect(() => { - if (!selected) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - setSelected(false); - } - }; - - document.addEventListener("keydown", handleKeyDown, true); - return () => { - document.removeEventListener("keydown", handleKeyDown, true); - }; - }, [selected]); - - return ( - - - - - {output.name.replace(/_/g, " ")} - - - {hasValue && ( - - {value} - - )} - - - - ); -}; - -const getOutputHandleId = (outputName: string) => { - return `output_${outputName}`; -}; - -const getInputHandleId = (inputName: string) => { - return `input_${inputName}`; -}; - -const skipHandleDeselect = (e: MouseEvent) => { - let el = e.target as HTMLElement | null; - while (el) { - if ( - el instanceof HTMLElement && - (el.getAttribute("data-sidebar") === "sidebar" || - el.getAttribute("role") === "dialog") - ) { - return true; - } - el = el.parentElement; - } - return false; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx deleted file mode 100644 index 7a29a1bb7a..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { useNavigate } from "@tanstack/react-router"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { PublishedComponentBadge } from "@/components/shared/ManageComponent/PublishedComponentBadge"; -import { trimDigest } from "@/components/shared/ManageComponent/utils/digest"; -import { useFlagValue } from "@/components/shared/Settings/useFlags"; -import TaskStatusBar from "@/components/shared/Status/TaskStatusBar"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { QuickTooltip } from "@/components/ui/tooltip"; -import { Text } from "@/components/ui/typography"; -import { useEdgeSelectionHighlight } from "@/hooks/useEdgeSelectionHighlight"; -import { useIsMultiSelect } from "@/hooks/useIsMultiSelect"; -import { buildExecutionUrl } from "@/hooks/useSubgraphBreadcrumbs"; -import { cn } from "@/lib/utils"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useContextPanel } from "@/providers/ContextPanelProvider"; -import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; -import { useTaskNode } from "@/providers/TaskNodeProvider"; -import { isPipelineAggregator } from "@/utils/annotations"; -import { isCacheDisabled } from "@/utils/cache"; -import type { ExecutionStatusStats } from "@/utils/executionStatus"; -import { getSubgraphDescription, isSubgraph } from "@/utils/subgraphUtils"; - -import { - type NotifyMessage, - type UpdateOverlayMessage, - useNodesOverlay, -} from "../../../NodesOverlay/NodesOverlayProvider"; -import TaskOverview from "../TaskOverview"; -import { TaskNodeInputs } from "./TaskNodeInputs"; -import { TaskNodeOutputs } from "./TaskNodeOutputs"; -import { UpgradeNodePopover } from "./UpgradeNodePopover"; - -const TaskNodeCard = () => { - const navigate = useNavigate(); - const inputAggregatorEnabled = useFlagValue("input-aggregator"); - - const { registerNode } = useNodesOverlay(); - const taskNode = useTaskNode(); - const { - setContent, - clearContent, - setOpen: setContextPanelOpen, - } = useContextPanel(); - const { navigateToSubgraph } = useComponentSpec(); - const executionData = useExecutionDataOptional(); - const runId = executionData?.runId; - const details = executionData?.details; - const executionState = executionData?.state; - - const nodeRef = useRef(null); - - const [updateOverlayDialogOpen, setUpdateOverlayDialogOpen] = useState< - UpdateOverlayMessage["data"] | undefined - >(); - const [highlightedState, setHighlighted] = useState(false); - - const [expandedInputs, setExpandedInputs] = useState(false); - const [expandedOutputs, setExpandedOutputs] = useState(false); - - const { name, displayName, state, nodeId, taskSpec, taskId } = taskNode; - const { dimensions, selected, highlighted, readOnly, isCollapsed } = state; - - const { isMultiSelect, isMultiSelectRef } = useIsMultiSelect(); - - const { isConnectedToSelectedEdge } = useEdgeSelectionHighlight(nodeId); - - const isSubgraphNode = useMemo(() => { - if (!taskSpec) return false; - return isSubgraph(taskSpec); - }, [taskSpec]); - - const subgraphExecutionStats = useMemo((): ExecutionStatusStats | null => { - if (!isSubgraphNode || !taskId) return null; - - const executionId = details?.child_task_execution_ids?.[taskId]; - if (!executionId) return null; - - const stats = executionState?.child_execution_status_stats?.[executionId]; - if (!stats) return null; - - return stats; - }, [isSubgraphNode, taskId, details, executionState]); - - const subgraphDescription = useMemo(() => { - if (!taskSpec) return ""; - return getSubgraphDescription(taskSpec); - }, [taskSpec]); - - const disabledCache = isCacheDisabled(taskSpec); - - const showBetaPill = - !inputAggregatorEnabled && - isPipelineAggregator(taskSpec?.componentRef?.spec?.metadata?.annotations); - - const onNotify = useCallback((message: NotifyMessage) => { - switch (message.type) { - case "highlight": - setHighlighted(true); - break; - case "clear": - setHighlighted(false); - break; - case "update-overlay": - setHighlighted(true); - setUpdateOverlayDialogOpen({ - ...message.data, - }); - break; - } - }, []); - - useEffect(() => { - if (!taskSpec) return; - return registerNode({ - nodeId, - taskSpec, - onNotify, - }); - }, [registerNode, nodeId, taskSpec, onNotify]); - - const closeOverlayPopover = useCallback((open: boolean) => { - setHighlighted(open); - - if (!open) { - setUpdateOverlayDialogOpen(undefined); - } - }, []); - - const taskConfigMarkup = useMemo( - () => , - [taskNode, nodeId], - ); - - const handleInputSectionClick = useCallback(() => { - setExpandedInputs((prev) => !prev); - }, []); - - const handleOutputSectionClick = useCallback(() => { - setExpandedOutputs((prev) => !prev); - }, []); - - const handleDoubleClick = useCallback(() => { - if (isSubgraphNode && taskId) { - navigateToSubgraph(taskId); - - if (runId && details?.child_task_execution_ids) { - const subgraphExecutionId = details.child_task_execution_ids[taskId]; - if (subgraphExecutionId) { - const url = buildExecutionUrl(runId, subgraphExecutionId); - navigate({ to: url }); - } - } - } - }, [isSubgraphNode, taskId, navigateToSubgraph, runId, details, navigate]); - - useEffect(() => { - if (selected && !isMultiSelect) { - setContent(taskConfigMarkup); - setContextPanelOpen(true); - } - - return () => { - if (selected && !isMultiSelectRef.current) { - clearContent(); - } - }; - }, [ - selected, - isMultiSelect, - taskConfigMarkup, - setContent, - clearContent, - setContextPanelOpen, - ]); - - if (!taskSpec) { - return null; - } - - const digestMarkup = taskSpec.componentRef?.digest && ( - - - {trimDigest(taskSpec.componentRef.digest)} - - - ); - - return ( - - - - - {isSubgraphNode && ( - - - - )} - {disabledCache && !readOnly && ( - - - - )} - - {displayName} - - {showBetaPill && ( - - - Beta - - - )} - - {displayName !== name && ( - - {name} - - )} - {displayName === name && - taskId && - taskId !== name && - !taskId.match(new RegExp(`^${name}\\s*\\d+$`)) && ( - - {taskId} - - )} - - - - {digestMarkup} - - - - {isSubgraphNode && subgraphExecutionStats && ( - - )} - - - - - - {updateOverlayDialogOpen ? ( - - ) : null} - - - ); -}; - -export default TaskNodeCard; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeInputs.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeInputs.test.tsx deleted file mode 100644 index d79f42b65b..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeInputs.test.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { ReactFlowProvider } from "@xyflow/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { TaskNodeInputs } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeInputs"; -import { useTaskNode } from "@/providers/TaskNodeProvider"; -import type { ArgumentType, InputSpec, TaskSpec } from "@/utils/componentSpec"; - -vi.mock("@/providers/ComponentLibraryProvider", () => ({ - useComponentLibrary: () => ({ - setSearchTerm: vi.fn(), - setSearchFilters: vi.fn(), - searchTerm: "", - searchFilters: [], - highlightSearchResults: false, - setHighlightSearchResults: vi.fn(), - }), -})); - -vi.mock("@/providers/ComponentSpecProvider", () => ({ - useComponentSpec: () => ({ - graphSpec: { - tasks: {}, - }, - }), -})); - -vi.mock("@/providers/TaskNodeProvider"); - -const TestWrapper = ReactFlowProvider; - -describe("", () => { - const mockUseTaskNode = vi.mocked(useTaskNode); - - const useMockedUseTaskNode = (inputs: InputSpec[], taskSpec: TaskSpec) => { - mockUseTaskNode.mockReturnValue({ - inputs, - taskSpec, - state: { readOnly: false }, - nodeId: "test-node", - callbacks: { onSelect: vi.fn() }, - } as any); - }; - - const createMockInput = ( - name: string, - type: string = "String", - optional: boolean = false, - defaultValue?: string, - ): InputSpec => ({ - name, - type, - optional, - default: defaultValue, - }); - - const createMockTaskSpec = ( - taskArguments?: Record, - ): TaskSpec => ({ - componentRef: { digest: "test-digest" }, - arguments: taskArguments, - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("invalid arguments", () => { - it("should render inputs and mark invalid ones correctly", () => { - const inputs = [ - createMockInput("validInput", "String", false), - createMockInput("invalidInput", "String", false), - createMockInput("optionalInput", "String", true), - ]; - const taskSpec = createMockTaskSpec({ - validInput: "some value", - // invalidInput is missing, should be marked as invalid - // optionalInput is optional, so it's valid even without value - }); - - useMockedUseTaskNode(inputs, taskSpec); - - render(, { - wrapper: TestWrapper, - }); - - // Check that all inputs are rendered - expect(screen.getByTestId("input-handle-validInput")).toBeInTheDocument(); - expect( - screen.getByTestId("input-handle-invalidInput"), - ).toBeInTheDocument(); - expect( - screen.getByTestId("input-handle-optionalInput"), - ).toBeInTheDocument(); - - // Check invalid marking - expect(screen.getByTestId("input-handle-validInput")).toHaveAttribute( - "data-invalid", - "false", - ); - expect(screen.getByTestId("input-handle-invalidInput")).toHaveAttribute( - "data-invalid", - "true", - ); - expect(screen.getByTestId("input-handle-optionalInput")).toHaveAttribute( - "data-invalid", - "false", - ); - }); - - it("should show hidden invalid arguments warning in collapsed mode", () => { - const inputs = [ - createMockInput("visibleInput", "String", false), - createMockInput("hiddenInvalidInput1", "String", false), - createMockInput("hiddenInvalidInput2", "String", false), - ]; - const taskSpec = createMockTaskSpec({ - visibleInput: { - taskOutput: { taskId: "task1", outputName: "output1" }, - }, - // hiddenInvalidInput1 and hiddenInvalidInput2 are missing arguments - }); - - useMockedUseTaskNode(inputs, taskSpec); - - render(, { - wrapper: TestWrapper, - }); - - // Should show warning about hidden invalid inputs - expect( - screen.getByText("2 hidden inputs have invalid arguments"), - ).toBeInTheDocument(); - }); - - it("should show singular form for single hidden invalid argument", () => { - const inputs = [ - createMockInput("visibleInput", "String", false), - createMockInput("hiddenInvalidInput", "String", false), - ]; - const taskSpec = createMockTaskSpec({ - visibleInput: { - taskOutput: { taskId: "task1", outputName: "output1" }, - }, - // hiddenInvalidInput is missing argument - }); - - useMockedUseTaskNode(inputs, taskSpec); - - render(, { - wrapper: TestWrapper, - }); - - // Should show singular form - expect( - screen.getByText("1 hidden input has invalid arguments"), - ).toBeInTheDocument(); - }); - - it("should not show hidden invalid arguments warning when there are none", () => { - const inputs = [ - createMockInput("visibleInput", "String", false), - createMockInput("hiddenValidInput", "String", true), // optional - ]; - const taskSpec = createMockTaskSpec({ - visibleInput: { - taskOutput: { taskId: "task1", outputName: "output1" }, - }, - }); - - useMockedUseTaskNode(inputs, taskSpec); - - render(, { - wrapper: TestWrapper, - }); - - // Should not show warning - expect( - screen.queryByText(/hidden input.*invalid arguments/), - ).not.toBeInTheDocument(); - }); - - it("should handle edge case with no inputs", () => { - useMockedUseTaskNode([], createMockTaskSpec()); - - render(, { - wrapper: TestWrapper, - }); - - // Component should not render anything when no inputs - expect(screen.queryByTestId(/input-handle-/)).not.toBeInTheDocument(); - }); - - it("should correctly identify and display all invalid argument scenarios", () => { - const inputs = [ - createMockInput("required_no_arg", "String", false), // Invalid: required, no argument, no default - createMockInput("required_with_arg", "String", false), // Valid: required, has argument - createMockInput("required_with_default", "String", false, "default"), // Valid: required, has default - createMockInput("optional_no_arg", "String", true), // Valid: optional - createMockInput("optional_with_arg", "String", true), // Valid: optional with argument - createMockInput("task_output_connected", "String", false), // Valid: connected to task output - ]; - - const taskSpec = createMockTaskSpec({ - required_with_arg: "some value", - optional_with_arg: "optional value", - task_output_connected: { - taskOutput: { taskId: "upstream", outputName: "result" }, - }, - // required_no_arg is missing - should be invalid - // required_with_default has default - should be valid - // optional_no_arg is optional - should be valid - }); - - useMockedUseTaskNode(inputs, taskSpec); - - render(, { - wrapper: TestWrapper, - }); - - // Check invalid markings - expect( - screen.getByTestId("input-handle-required_no_arg"), - ).toHaveAttribute("data-invalid", "true"); - expect( - screen.getByTestId("input-handle-required_with_arg"), - ).toHaveAttribute("data-invalid", "false"); - expect( - screen.getByTestId("input-handle-required_with_default"), - ).toHaveAttribute("data-invalid", "false"); - expect( - screen.getByTestId("input-handle-optional_no_arg"), - ).toHaveAttribute("data-invalid", "false"); - expect( - screen.getByTestId("input-handle-optional_with_arg"), - ).toHaveAttribute("data-invalid", "false"); - expect( - screen.getByTestId("input-handle-task_output_connected"), - ).toHaveAttribute("data-invalid", "false"); - }); - }); -}); diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeInputs.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeInputs.tsx deleted file mode 100644 index 6c04dc27b1..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeInputs.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { Handle, Position } from "@xyflow/react"; -import { useConnection } from "@xyflow/react"; -import { AlertCircle } from "lucide-react"; -import { - type MouseEvent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; - -import { useFlagValue } from "@/components/shared/Settings/useFlags"; -import { InlineStack } from "@/components/ui/layout"; -import { Text } from "@/components/ui/typography"; -import { cn } from "@/lib/utils"; -import { useForcedSearchContext } from "@/providers/ComponentLibraryProvider/ForcedSearchProvider"; -import { isValidFilterRequest } from "@/providers/ComponentLibraryProvider/types"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useTaskNode } from "@/providers/TaskNodeProvider"; -import { inputsWithInvalidArguments } from "@/services/componentService"; -import { AGGREGATOR_ADD_INPUT_HANDLE_ID } from "@/utils/aggregatorInputs"; -import { isPipelineAggregator } from "@/utils/annotations"; -import { - type InputSpec, - isGraphInputArgument, - isTaskOutputArgument, -} from "@/utils/componentSpec"; -import { ComponentSearchFilter } from "@/utils/constants"; -import { inputNameToNodeId } from "@/utils/nodes/nodeIdUtils"; -import { checkArtifactMatchesSearchFilters } from "@/utils/searchUtils"; - -import { InputHandle } from "./Handles"; -import { getDisplayValue } from "./handleUtils"; - -interface TaskNodeInputsProps { - collapsed: boolean; - expanded: boolean; - onBackgroundClick?: () => void; -} - -export function TaskNodeInputs({ - collapsed: condensedProp, - expanded, - onBackgroundClick, -}: TaskNodeInputsProps) { - const taskNode = useTaskNode(); - const { inputs, taskSpec, state, callbacks } = taskNode; - const { onSelect } = callbacks; - const { graphSpec } = useComponentSpec(); - const isPipelineAggregatorEnabled = useFlagValue("input-aggregator"); - const { - highlightSearchFilter, - resetSearchFilter, - currentSearchFilter, - highlightSearchResults, - } = useForcedSearchContext(); - - const connection = useConnection(); - - const [isDragging, setIsDragging] = useState(false); - - const isAggregator = - isPipelineAggregatorEnabled && - isPipelineAggregator(taskSpec?.componentRef?.spec?.metadata?.annotations); - - const internalInputs = useMemo( - () => [AGGREGATOR_ADD_INPUT_HANDLE_ID, "output_type"], - [], - ); - - const visibleInputs = useMemo(() => { - const filtered = isAggregator - ? inputs.filter((input) => !internalInputs.includes(input.name)) - : inputs; - - return filtered; - }, [isAggregator, inputs, internalInputs]); - - const values = taskSpec?.arguments; - const invalidArguments = taskSpec - ? inputsWithInvalidArguments(visibleInputs, taskSpec) - : []; - - const connectedInputs = useMemo( - () => - visibleInputs.filter( - (input) => - isGraphInputArgument(values?.[input.name]) || - isTaskOutputArgument(values?.[input.name]), - ), - [visibleInputs, values], - ); - - const toggleHighlightRelatedHandles = useCallback( - (selected: boolean, input?: InputSpec) => { - if (selected && input) { - const type = (input.type as string) || "[type undefined]"; - - highlightSearchFilter({ - searchTerm: type, - filters: [ - ComponentSearchFilter.OUTPUTTYPE, - ComponentSearchFilter.EXACTMATCH, - ], - }); - } else { - resetSearchFilter(); - } - }, - [highlightSearchFilter, resetSearchFilter], - ); - - const handleBackgroundClick = useCallback( - (e: MouseEvent) => { - if (condensedProp && onBackgroundClick) { - e.stopPropagation(); - onBackgroundClick(); - } - }, - [condensedProp, onBackgroundClick], - ); - - const handleSelectionChange = useCallback( - (inputName: string, selected: boolean) => { - if (state.readOnly) return; - - const input = visibleInputs.find((i) => i.name === inputName); - toggleHighlightRelatedHandles(selected, input); - }, - [visibleInputs, state.readOnly, toggleHighlightRelatedHandles], - ); - - const checkHighlight = useCallback( - (input: InputSpec) => { - if ( - !highlightSearchResults || - !isValidFilterRequest(currentSearchFilter, { - includesFilter: ComponentSearchFilter.INPUTTYPE, - }) - ) { - return false; - } - - const matchFound = checkArtifactMatchesSearchFilters( - currentSearchFilter.searchTerm, - currentSearchFilter.filters, - input, - ); - - return matchFound; - }, - [currentSearchFilter, highlightSearchResults], - ); - - const handleLabelClick = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - onSelect?.(); - }, - [onSelect], - ); - - useEffect(() => { - // Highlight relevant Handles when the user drags a new connection - const { fromHandle, from, to, inProgress } = connection; - - if (!inProgress) { - resetSearchFilter(); - setIsDragging(false); - return; - } - - if (isDragging) { - return; - } - - if ( - from && - to && - Math.sqrt(Math.pow(from.x - to.x, 2) + Math.pow(from.y - to.y, 2)) < 4 - ) { - // If the user has dragged the cursor less than 4px from the click origin, then assume it is a click event on the Handle - setIsDragging(false); - return; - } - - const input = visibleInputs.find( - (i) => inputNameToNodeId(i.name) === fromHandle?.id, - ); - - if (!input) return; - - toggleHighlightRelatedHandles(true, input); - setIsDragging(true); - }, [ - connection, - visibleInputs, - isDragging, - resetSearchFilter, - toggleHighlightRelatedHandles, - ]); - - if (!taskSpec) { - return null; - } - - if (!visibleInputs.length && !isAggregator) return null; - - const displayInputs = useMemo(() => { - // For aggregators, always show all visible inputs (not just connected ones) - // This ensures handles exist for incoming connections - if (isAggregator) { - return visibleInputs; - } - - // For regular components, show only connected inputs or the first input - return connectedInputs.length === 0 && visibleInputs.length > 0 - ? [visibleInputs[0]] - : connectedInputs; - }, [isAggregator, visibleInputs, connectedInputs]); - - const hiddenInputs = visibleInputs.length - displayInputs.length; - const condensed = condensedProp && hiddenInputs >= 1; - - const hiddenInvalidArguments = invalidArguments.filter( - (invalidArgument) => - !displayInputs.some((input) => input.name === invalidArgument), - ); - - return ( - - {isAggregator && !state.readOnly && ( - - - - - - - + Add Input - - - - )} - {condensed && !expanded ? ( - <> - {displayInputs.map((input, i) => ( - 0 && i === 0 - ? `+${hiddenInputs} more input${hiddenInputs > 1 ? "s" : ""}` - : " " - } - rawValue={values?.[input.name]} - onHandleSelectionChange={handleSelectionChange} - highlight={checkHighlight(input)} - onLabelClick={handleLabelClick} - /> - ))} - {hiddenInvalidArguments.length > 0 && ( - - - {`${hiddenInvalidArguments.length} hidden input${hiddenInvalidArguments.length > 1 ? "s have" : " has"} invalid arguments`} - - )} - > - ) : ( - <> - {visibleInputs.map((input) => ( - - ))} - {condensed && ( - - (Click to collapse) - - )} - > - )} - - ); -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeOutputs.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeOutputs.tsx deleted file mode 100644 index 3718283394..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeOutputs.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useConnection, useEdges } from "@xyflow/react"; -import { type MouseEvent, useCallback, useEffect, useState } from "react"; - -import { OutputTypeSelector } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/OutputTypeSelector/OutputTypeSelector"; -import { useFlagValue } from "@/components/shared/Settings/useFlags"; -import { cn } from "@/lib/utils"; -import { useForcedSearchContext } from "@/providers/ComponentLibraryProvider/ForcedSearchProvider"; -import { isValidFilterRequest } from "@/providers/ComponentLibraryProvider/types"; -import { useTaskNode } from "@/providers/TaskNodeProvider"; -import { AggregatorOutputType } from "@/types/aggregator"; -import { isPipelineAggregator } from "@/utils/annotations"; -import type { OutputSpec } from "@/utils/componentSpec"; -import { ComponentSearchFilter } from "@/utils/constants"; -import { outputNameToNodeId } from "@/utils/nodes/nodeIdUtils"; -import { checkArtifactMatchesSearchFilters } from "@/utils/searchUtils"; - -import { OutputHandle } from "./Handles"; - -type TaskNodeOutputsProps = { - collapsed: boolean; - expanded: boolean; - onBackgroundClick?: () => void; -}; - -export function TaskNodeOutputs({ - collapsed, - expanded, - onBackgroundClick, -}: TaskNodeOutputsProps) { - const { nodeId, outputs, state, taskSpec, callbacks } = useTaskNode(); - const { onSelect } = callbacks; - const isPipelineAggregatorEnabled = useFlagValue("input-aggregator"); - const { - highlightSearchFilter, - resetSearchFilter, - currentSearchFilter, - highlightSearchResults, - } = useForcedSearchContext(); - - const connection = useConnection(); - const edges = useEdges(); - - const [isDragging, setIsDragging] = useState(false); - - const isAggregator = - isPipelineAggregatorEnabled && - isPipelineAggregator(taskSpec?.componentRef?.spec?.metadata?.annotations); - - // For aggregators, get the current output type from the output_type input argument - const rawOutputType = isAggregator - ? taskSpec?.arguments?.["output_type"] - : undefined; - const currentOutputType = - isAggregator && - (Object.values(AggregatorOutputType) as string[]).includes( - rawOutputType as string, - ) - ? (rawOutputType as AggregatorOutputType) - : AggregatorOutputType.JsonArray; - - // Local state to immediately reflect dropdown changes - const [selectedOutputType, setSelectedOutputType] = - useState(currentOutputType); - - // Sync local state with props when taskSpec changes - useEffect(() => { - setSelectedOutputType(currentOutputType); - }, [currentOutputType]); - - const outputsWithTaskInput = outputs.filter((output) => - edges.some( - (edge) => - edge.source === nodeId && - edge.sourceHandle === outputNameToNodeId(output.name), - ), - ); - - const toggleHighlightRelatedHandles = useCallback( - (selected: boolean, output?: OutputSpec) => { - if (selected && output) { - const type = (output.type as string) || "[type undefined]"; - - highlightSearchFilter({ - searchTerm: type, - filters: [ - ComponentSearchFilter.INPUTTYPE, - ComponentSearchFilter.EXACTMATCH, - ], - }); - } else { - resetSearchFilter(); - } - }, - [highlightSearchFilter, resetSearchFilter], - ); - - const handleBackgroundClick = useCallback( - (e: MouseEvent) => { - if (collapsed && onBackgroundClick) { - e.stopPropagation(); - onBackgroundClick(); - } - }, - [collapsed, onBackgroundClick], - ); - - const handleSelectionChange = useCallback( - (outputName: string, selected: boolean) => { - if (state.readOnly) return; - - const output = outputs.find((o) => o.name === outputName); - toggleHighlightRelatedHandles(selected, output); - }, - [outputs, state.readOnly, toggleHighlightRelatedHandles], - ); - - const checkHighlight = useCallback( - (output: OutputSpec) => { - if ( - !highlightSearchResults || - !isValidFilterRequest(currentSearchFilter, { - includesFilter: ComponentSearchFilter.OUTPUTTYPE, - }) - ) { - return false; - } - - const matchFound = checkArtifactMatchesSearchFilters( - currentSearchFilter?.searchTerm, - currentSearchFilter?.filters, - output, - ); - - return matchFound; - }, - [highlightSearchResults, currentSearchFilter], - ); - - const handleLabelClick = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - onSelect?.(); - }, - [onSelect], - ); - - useEffect(() => { - // Highlight relevant Handles when the user drags a new connection - const { fromHandle, from, to, inProgress } = connection; - - if (!inProgress) { - resetSearchFilter(); - setIsDragging(false); - return; - } - - if (isDragging) { - return; - } - - if ( - from && - to && - Math.sqrt(Math.pow(from.x - to.x, 2) + Math.pow(from.y - to.y, 2)) < 4 - ) { - // If the user has dragged the cursor less than 4px from the click origin, then assume it is a click event on the Handle - setIsDragging(false); - return; - } - - const output = outputs.find( - (o) => outputNameToNodeId(o.name) === fromHandle?.id, - ); - - if (!output) return; - - toggleHighlightRelatedHandles(true, output); - setIsDragging(true); - }, [ - connection, - outputs, - isDragging, - toggleHighlightRelatedHandles, - resetSearchFilter, - ]); - - if (!outputs.length) return null; - - if (outputsWithTaskInput.length === 0) { - outputsWithTaskInput.push(outputs[0]); - } - - const hiddenOutputs = outputs.length - outputsWithTaskInput.length; - if (hiddenOutputs < 1) { - collapsed = false; - } - - return ( - - {isAggregator && !state.readOnly && ( - - { - // Update local state immediately for UI responsiveness - setSelectedOutputType(value); - - // Update the output_type input argument - callbacks.setArguments({ - ...taskSpec?.arguments, - output_type: value, - }); - }} - /> - - )} - {collapsed && !expanded ? ( - outputsWithTaskInput.map((output, i) => ( - 0 && i === 0 - ? `+${hiddenOutputs} more output${hiddenOutputs > 1 ? "s" : ""}` - : " " - } - onHandleSelectionChange={handleSelectionChange} - highlight={checkHighlight(output)} - onLabelClick={handleLabelClick} - /> - )) - ) : ( - <> - {outputs.map((output) => ( - - ))} - {collapsed && ( - - (Click to collapse) - - )} - > - )} - - ); -} diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/UpgradeNodePopover.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/UpgradeNodePopover.tsx deleted file mode 100644 index d2d5b4c5df..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/UpgradeNodePopover.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { type ComponentProps, useCallback, useMemo, useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Heading } from "@/components/ui/typography"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import type { TaskNodeContextType } from "@/providers/TaskNodeProvider"; -import type { HydratedComponentReference } from "@/utils/componentSpec"; - -import { - type UpdateOverlayMessage, - useNodesOverlay, -} from "../../../NodesOverlay/NodesOverlayProvider"; -import { getUpgradeConfirmationDetails } from "../../ConfirmationDialogs/UpgradeComponent"; -import { replaceTaskNode } from "../../utils/replaceTaskNode"; - -/** - * Popover that appears when a task is upgraded. - * - * @param currentNode - The current task node - * @param ids - The ids of the nodes to upgrade - * @param replaceWith - The component reference to replace the task with - * @param onOpenChange - The function to call when the popover is opened or closed - * @param props - The props for the popover - * - * @returns The UpgradeNodePopover component - */ -export const UpgradeNodePopover = ({ - currentNode, - ids, - replaceWith, - onOpenChange, - ...props -}: ComponentProps & - UpdateOverlayMessage["data"] & { - currentNode: TaskNodeContextType; - }) => { - const [open, setOpen] = useState(true); - const { taskId, taskSpec } = currentNode; - const { currentGraphSpec, updateGraphSpec } = useComponentSpec(); - const { notifyNode, fitNodeIntoView } = useNodesOverlay(); - - const replaceWithComponent = useMemo(() => { - if (!taskSpec) return null; - return replaceWith.get(taskSpec.componentRef.digest ?? "") as - HydratedComponentReference | undefined; - }, [replaceWith, taskSpec]); - - const updatePreview = useMemo(() => { - if (!taskId || !replaceWithComponent) return null; - return replaceTaskNode(taskId, replaceWithComponent, currentGraphSpec); - }, [taskId, replaceWithComponent, currentGraphSpec]); - - const markup = useMemo(() => { - if (!taskId || !taskSpec || !replaceWithComponent || !updatePreview) { - return null; - } - const { content } = getUpgradeConfirmationDetails( - taskId, - taskSpec, - replaceWithComponent.digest, - updatePreview.lostInputs, - ); - - return content; - }, [taskId, taskSpec, replaceWithComponent, updatePreview]); - - const handleOpenChange = useCallback(() => { - setOpen(false); - onOpenChange?.(false); - }, [onOpenChange]); - - const handleNext = useCallback(async () => { - handleOpenChange(); - - if (ids.length === 0) { - return; - } - - const nodeId = ids.pop(); - - if (!nodeId) { - return; - } - - await fitNodeIntoView(nodeId); - - notifyNode(nodeId, { - type: "update-overlay", - data: { - replaceWith, - ids, - }, - }); - }, [fitNodeIntoView, ids, replaceWith, notifyNode, handleOpenChange]); - - const handleApplyAndNext = useCallback(async () => { - if (!updatePreview) return; - updateGraphSpec(updatePreview.updatedGraphSpec); - - void handleNext(); - }, [handleNext, updatePreview, updateGraphSpec]); - - if (!taskSpec || !taskId || !replaceWithComponent || !updatePreview) { - return null; - } - - return ( - - - - - Upgrade task - {markup} - - - Skip - - - {ids.length > 0 ? "Accept and move to next task" : "Accept"} - - - - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/index.ts b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/index.ts deleted file mode 100644 index 1634a1c325..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TaskNodeCard } from "./TaskNodeCard"; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/ConfigurationSection.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/ConfigurationSection.tsx deleted file mode 100644 index c2f76ceea6..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/ConfigurationSection.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { BlockStack } from "@/components/ui/layout"; -import { Separator } from "@/components/ui/separator"; -import { Paragraph } from "@/components/ui/typography"; -import { type TaskNodeContextType } from "@/providers/TaskNodeProvider"; -import { isGraphImplementation } from "@/utils/componentSpec"; - -import { AnnotationsSection } from "../AnnotationsEditor/AnnotationsSection"; -import TaskConfiguration from "./TaskConfiguration"; - -interface ConfigurationSectionProps { - taskNode: TaskNodeContextType; -} - -const ConfigurationSection = ({ taskNode }: ConfigurationSectionProps) => { - const { taskSpec, callbacks } = taskNode; - - if (!taskSpec) { - return null; - } - - const componentSpec = taskSpec.componentRef.spec; - - if (!componentSpec) { - console.error( - "TaskOverview called with missing taskSpec.componentRef.spec", - ); - return null; - } - - const isSubgraph = isGraphImplementation(componentSpec.implementation); - - return ( - - - Configure task annotations, resources and custom data. - - - {!isSubgraph && ( - <> - - - > - )} - - - - ); -}; - -export default ConfigurationSection; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/OutputsList.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/OutputsList.tsx deleted file mode 100644 index 623c2d7b3b..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/OutputsList.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { BlockStack } from "@/components/ui/layout"; -import { Heading } from "@/components/ui/typography"; -import type { TaskSpec } from "@/utils/componentSpec"; - -interface OutputsListProps { - taskSpec: TaskSpec; -} - -const OutputsList = ({ taskSpec }: OutputsListProps) => { - const componentSpec = taskSpec.componentRef.spec; - - const outputs = componentSpec?.outputs; - - const outputsMarkup = outputs?.map((output) => ( - - - {output.name} - {output.type && ( - - ({output.type as string}) - - )} - {output.description && ( - - : {output.description} - - )} - - - )); - - return ( - - Outputs - {outputsMarkup} - - ); -}; - -export default OutputsList; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/RenameTask.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/RenameTask.tsx deleted file mode 100644 index 35aa9434d4..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/RenameTask.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useState } from "react"; - -import { InputDialog } from "@/components/shared/Dialogs/InputDialog"; -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { isGraphImplementation } from "@/utils/componentSpec"; -import { updateSubgraphSpec } from "@/utils/subgraphUtils"; -import { validateTaskName } from "@/utils/unique"; - -interface RenameTaskProps { - taskId: string; -} - -const RenameTask = ({ taskId }: RenameTaskProps) => { - const { currentSubgraphSpec, currentSubgraphPath, setComponentSpec } = - useComponentSpec(); - const [isOpen, setIsOpen] = useState(false); - - if (!isGraphImplementation(currentSubgraphSpec.implementation)) { - return null; - } - - const currentSubgraphGraphSpec = currentSubgraphSpec.implementation.graph; - const task = currentSubgraphGraphSpec.tasks[taskId]; - const name = task?.componentRef.spec?.name || ""; - - const componentSpec = task?.componentRef.spec; - - const isSubgraph = componentSpec?.implementation - ? isGraphImplementation(componentSpec?.implementation) - : false; - - const onConfirm = (newName: string) => { - const updatedSubgraphSpec = { ...currentSubgraphSpec }; - - if (isGraphImplementation(updatedSubgraphSpec.implementation)) { - const task = updatedSubgraphSpec.implementation.graph.tasks[taskId]; - - if (!task?.componentRef.spec) return; - - task.componentRef.spec.name = newName; - - const updatedRootSpec = updateSubgraphSpec( - currentSubgraphSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(updatedRootSpec); - } - setIsOpen(false); - }; - - const onCancel = () => { - setIsOpen(false); - }; - - const inputDialogProps = { - title: isSubgraph ? "Rename Subgraph" : "Rename Task", - description: `Enter a new name for the ${isSubgraph ? "subgraph" : "task"}: "${taskId}".`, - placeholder: isSubgraph ? "Subgraph Name" : "Task Name", - defaultValue: name, - validate: (value: string) => - validateTaskName(value, currentSubgraphGraphSpec), - onConfirm, - onCancel, - }; - - return ( - <> - setIsOpen(true)} - className="w-fit h-fit p-1 text-gray-500/50" - variant="ghost" - size="icon" - > - - - - > - ); -}; - -export default RenameTask; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskConfiguration.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskConfiguration.tsx deleted file mode 100644 index d98c5876fb..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskConfiguration.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useCallback } from "react"; - -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Switch } from "@/components/ui/switch"; -import { Heading, Paragraph } from "@/components/ui/typography"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { type TaskNodeContextType } from "@/providers/TaskNodeProvider"; -import { isCacheDisabled } from "@/utils/cache"; -import { ISO8601_DURATION_ZERO_DAYS } from "@/utils/constants"; - -interface TaskConfigurationProps { - taskNode: TaskNodeContextType; -} - -const TaskConfiguration = ({ taskNode }: TaskConfigurationProps) => { - const { taskSpec, callbacks } = taskNode; - const { track } = useAnalytics(); - - const handleDisableCacheChange = useCallback( - (checked: boolean) => { - track("pipeline_editor.task_node.disable_cache_toggle", { - new_value: checked, - }); - callbacks.setCacheStaleness( - checked ? ISO8601_DURATION_ZERO_DAYS : undefined, - ); - }, - [callbacks, track], - ); - - if (!taskSpec) { - return null; - } - - const disabledCache = isCacheDisabled(taskSpec); - - return ( - - Configuration - - - Disable cache - - - - - - Collapse node - - { - track("pipeline_editor.task_node.collapse_node_toggle", { - new_value: checked, - }); - taskNode.callbacks.setCollapsed(checked); - }} - /> - - - ); -}; - -export default TaskConfiguration; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx deleted file mode 100644 index 298dec613b..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { - AmphoraIcon, - FilePenLineIcon, - InfoIcon, - LogsIcon, - Parentheses, -} from "lucide-react"; - -import type { ContainerExecutionStatus } from "@/api/types.gen"; -import { ComponentDetailsDialog } from "@/components/shared/Dialogs"; -import { ComponentFavoriteToggle } from "@/components/shared/FavoriteComponentToggle"; -import { RemoteTroubleshootButton } from "@/components/shared/RemoteTroubleshootAction/RemoteTroubleshootButton"; -import { StatusIcon } from "@/components/shared/Status"; -import { TaskDetails } from "@/components/shared/TaskDetails"; -import TaskActions from "@/components/shared/TaskDetails/Actions"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Separator } from "@/components/ui/separator"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Text } from "@/components/ui/typography"; -import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; -import { type TaskNodeContextType } from "@/providers/TaskNodeProvider"; -import { isGraphImplementation } from "@/utils/componentSpec"; - -import ArgumentsSection from "../ArgumentsEditor/ArgumentsSection"; -import ConfigurationSection from "./ConfigurationSection"; -import IOSection from "./IOSection/IOSection"; -import Logs, { OpenLogsInNewWindowLink } from "./logs"; -import { LogsEventsOverlaySection } from "./LogsEventsOverlaySection"; -import OutputsList from "./OutputsList"; -import RenameTask from "./RenameTask"; -import { ZIndexEditor } from "./ZIndexEditor"; - -interface TaskOverviewProps { - taskNode: TaskNodeContextType; -} - -const TaskOverview = ({ taskNode }: TaskOverviewProps) => { - const { componentRef, displayName, taskSpec, taskId, state, callbacks } = - taskNode; - - const executionData = useExecutionDataOptional(); - const details = executionData?.details; - const runId = executionData?.runId; - - const { readOnly, status } = state; - const disabled = !!status; - - if (!taskSpec || !taskId) { - return null; - } - - const componentSpec = taskSpec.componentRef.spec; - - if (!componentSpec) { - console.error( - "TaskOverview called with missing taskSpec.componentRef.spec", - ); - return null; - } - - const isSubgraph = isGraphImplementation(componentSpec.implementation); - const executionId = details?.child_task_execution_ids?.[taskId]; - const canRename = !readOnly && isSubgraph; - - return ( - - - {isSubgraph && } - - {displayName} - - {canRename && } - - - {readOnly && } - - - {!!componentRef && ( - - )} - - {runId && ( - - )} - - - - - - {readOnly ? ( - - ) : ( - - )} - {readOnly ? "Artifacts" : "Arguments"} - - - - Details - - - {readOnly && !isSubgraph && ( - - - Logs - - )} - {!readOnly && ( - - - Configuration - - )} - - - - - - {!readOnly && ( - - - - - - )} - {readOnly && ( - - )} - - {readOnly && !isSubgraph && ( - - {!!executionId && ( - - - - )} - - - - )} - {!readOnly && ( - - - - )} - - - - - - ); -}; - -export default TaskOverview; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/ZIndexEditor.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/ZIndexEditor.tsx deleted file mode 100644 index f5f83f5b72..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/ZIndexEditor.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock"; -import type { TaskNodeContextType } from "@/providers/TaskNodeProvider"; -import { ZINDEX_ANNOTATION } from "@/utils/annotations"; - -import { StackingControls } from "../../../FlowControls/StackingControls"; - -interface ZIndexEditorProps { - taskNode: TaskNodeContextType; -} - -export const ZIndexEditor = ({ taskNode }: ZIndexEditorProps) => { - const handleStackingControlChange = (newZIndex: number) => { - const updatedAnnotations = { - ...taskNode.taskSpec?.annotations, - [ZINDEX_ANNOTATION]: `${newZIndex}`, - }; - - taskNode.callbacks.setAnnotations(updatedAnnotations); - }; - - return ( - - - - ); -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/index.ts b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/index.ts deleted file mode 100644 index fd01b21f17..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./TaskOverview"; diff --git a/src/components/shared/ReactFlow/FlowCanvas/index.ts b/src/components/shared/ReactFlow/FlowCanvas/index.ts deleted file mode 100644 index d1fd001c4e..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./FlowCanvas"; diff --git a/src/components/shared/ReactFlow/FlowCanvas/types.ts b/src/components/shared/ReactFlow/FlowCanvas/types.ts index 7223e0d007..0be2a05938 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/types.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/types.ts @@ -1,42 +1,20 @@ -import type { Edge, Node } from "@xyflow/react"; -import type { ComponentType } from "react"; +import type { Node } from "@xyflow/react"; -import type { TaskType } from "@/types/taskNode"; - -import SmoothEdge from "./Edges/SmoothEdge"; -import FlexNode from "./FlexNode/FlexNode"; import type { FlexNodeData } from "./FlexNode/types"; -import GhostNode from "./GhostNode/GhostNode"; -import IONode from "./IONode/IONode"; -import TaskNode from "./TaskNode/TaskNode"; - -export type NodesAndEdges = { - nodes: Node[]; - edges: Edge[]; -}; - -export const nodeTypes: Record> = { - task: TaskNode, - input: IONode, - output: IONode, - ghost: GhostNode, - flex: FlexNode, +const nodeTypes: Record = { + task: true, + input: true, + output: true, + ghost: true, + flex: true, }; export type NodeType = keyof typeof nodeTypes; -export const edgeTypes: Record> = { - customEdge: SmoothEdge, -}; - export function isDefinedNode(node: Node): node is Node & { type: NodeType } { return !!node.type && node.type in nodeTypes; } -export function isTaskNodeType(type: string): type is TaskType { - return type === "task" || type === "input" || type === "output"; -} - export function isFlexNode(node: Node): node is Node { return node.type === "flex"; } diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/createConnectedIONode.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/createConnectedIONode.ts deleted file mode 100644 index 2250921e36..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/createConnectedIONode.ts +++ /dev/null @@ -1,242 +0,0 @@ -import type { XYPosition } from "@xyflow/react"; - -import type { TaskType } from "@/types/taskNode"; -import { - AGGREGATOR_ADD_INPUT_HANDLE_ID, - createAggregatorInput, - getNextAggregatorInputName, -} from "@/utils/aggregatorInputs"; -import { isPipelineAggregator } from "@/utils/annotations"; -import type { - ComponentReference, - ComponentSpec, - GraphSpec, - TaskSpec, - TypeSpecType, -} from "@/utils/componentSpec"; -import { isGraphImplementation } from "@/utils/componentSpec"; -import { deepClone } from "@/utils/deepClone"; -import { - nodeIdToInputName, - nodeIdToOutputName, - nodeIdToTaskId, -} from "@/utils/nodes/nodeIdUtils"; -import { componentSpecToText } from "@/utils/yaml"; - -import addTask from "./addTask"; -import { setGraphOutputValue } from "./setGraphOutputValue"; -import { setTaskArgument } from "./setTaskArgument"; - -type CreateConnectedIONodeParams = { - componentSpec: ComponentSpec; - taskNodeId: string; - handleId: string; - position: XYPosition; - ioType: "input" | "output"; -}; - -/** - * Creates an input or output node and automatically connects it to a task's handle. - * - * This function: - * 1. Creates a new input/output node with an auto-generated name (e.g., "param_name input") - * 2. Adds it to the component spec at the specified position - * 3. Copies default values from the task's input spec (for input nodes) - * 4. Automatically wires the connection to the specified task handle - * - * For outputs: connects task output → graph output - * For inputs: connects graph input → task input, preserving default values - * - * @param componentSpec - The component specification to modify - * @param taskNodeId - The node ID of the task to connect to - * @param handleId - The handle ID on the task (e.g., "input_param_name" or "output_result") - * @param position - Where to place the new input/output node - * @param ioType - Whether to create an "input" or "output" node - * @returns Updated component spec with the new connected IO node - */ -export const createConnectedIONode = ({ - componentSpec, - taskNodeId, - handleId, - position, - ioType, -}: CreateConnectedIONodeParams): ComponentSpec => { - if (!isGraphImplementation(componentSpec.implementation)) { - return componentSpec; - } - - const taskId = nodeIdToTaskId(taskNodeId); - const taskSpec = componentSpec.implementation.graph.tasks[taskId]; - const taskComponentSpec = taskSpec?.componentRef?.spec; - - if ( - ioType === "input" && - handleId === AGGREGATOR_ADD_INPUT_HANDLE_ID && - taskSpec && - taskComponentSpec && - isPipelineAggregator(taskComponentSpec.metadata?.annotations) - ) { - return createConnectedAggregatorInputNode({ - componentSpec, - taskId, - taskSpec, - taskComponentSpec, - position, - }); - } - - const argName = - ioType === "input" - ? nodeIdToInputName(handleId) - : nodeIdToOutputName(handleId); - const taskType: TaskType = ioType; - - let ioNodeType: TypeSpecType | undefined; - let ioNodeDefault: string | undefined; - - if (ioType === "input") { - const inputSpec = taskComponentSpec?.inputs?.find( - (i) => i.name === argName, - ); - ioNodeType = inputSpec?.type; - const taskArgValue = taskSpec?.arguments?.[argName]; - ioNodeDefault = - typeof taskArgValue === "string" ? taskArgValue : inputSpec?.default; - } else { - const outputSpec = taskComponentSpec?.outputs?.find( - (o) => o.name === argName, - ); - ioNodeType = outputSpec?.type; - } - - const { spec: specWithIO, ioName: createdIOName } = addTask( - taskType, - null, - position, - componentSpec, - { - name: argName, - type: ioNodeType, - ...(ioNodeDefault && { default: ioNodeDefault }), - }, - ); - - if (!isGraphImplementation(specWithIO.implementation)) { - return componentSpec; - } - - if (!createdIOName) { - return specWithIO; - } - - let updatedGraph: GraphSpec; - - if (ioType === "output") { - updatedGraph = setGraphOutputValue( - specWithIO.implementation.graph, - createdIOName, - { - taskOutput: { - taskId, - outputName: argName, - }, - }, - ); - } else { - updatedGraph = setTaskArgument( - specWithIO.implementation.graph, - taskId, - argName, - { - graphInput: { - inputName: createdIOName, - }, - }, - ); - } - - return { - ...specWithIO, - implementation: { - graph: updatedGraph, - }, - }; -}; - -type CreateConnectedAggregatorInputNodeParams = { - componentSpec: ComponentSpec; - taskId: string; - taskSpec: TaskSpec; - taskComponentSpec: ComponentSpec; - position: XYPosition; -}; - -const createConnectedAggregatorInputNode = ({ - componentSpec, - taskId, - taskSpec, - taskComponentSpec, - position, -}: CreateConnectedAggregatorInputNodeParams): ComponentSpec => { - const newAggInputName = getNextAggregatorInputName( - taskComponentSpec.inputs ?? [], - ); - const newAggInput = createAggregatorInput(newAggInputName); - - const clonedSpec: ComponentSpec = deepClone(taskComponentSpec); - const updatedTaskInputs = [...(clonedSpec.inputs ?? []), newAggInput]; - - const orderedTaskSpec: ComponentSpec = { - name: clonedSpec.name, - ...(clonedSpec.description && { description: clonedSpec.description }), - ...(clonedSpec.metadata && { metadata: clonedSpec.metadata }), - inputs: updatedTaskInputs, - ...(clonedSpec.outputs && { outputs: clonedSpec.outputs }), - implementation: clonedSpec.implementation, - }; - - const updatedComponentRef: ComponentReference = { - ...taskSpec.componentRef, - spec: orderedTaskSpec, - text: componentSpecToText(orderedTaskSpec), - }; - - const specWithUpdatedTask = componentSpec; - - if (isGraphImplementation(specWithUpdatedTask.implementation)) { - specWithUpdatedTask.implementation.graph.tasks[taskId] = { - ...taskSpec, - componentRef: updatedComponentRef, - }; - } - - const { spec: specWithGraphInput, ioName: createdGraphInputName } = addTask( - "input", - null, - position, - specWithUpdatedTask, - { - name: newAggInputName, - type: newAggInput.type, - }, - ); - - if ( - !createdGraphInputName || - !isGraphImplementation(specWithGraphInput.implementation) - ) { - return specWithGraphInput; - } - - const updatedGraph = setTaskArgument( - specWithGraphInput.implementation.graph, - taskId, - newAggInputName, - { graphInput: { inputName: createdGraphInputName } }, - ); - - return { - ...specWithGraphInput, - implementation: { graph: updatedGraph }, - }; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/getNodeFromEvent.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/getNodeFromEvent.ts deleted file mode 100644 index df85b97bb8..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/getNodeFromEvent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DragEvent } from "react"; - -import type { TaskSpec } from "@/utils/componentSpec"; - -import type { NodeType } from "../types"; - -export const getNodeFromEvent = (event: DragEvent) => { - const droppedData = event.dataTransfer.getData("application/reactflow"); - - if (droppedData === "") { - return { spec: null, nodeType: null }; - } - - const droppedDataObject = JSON.parse(droppedData); - const nodeType = Object.keys(droppedDataObject)[0] as NodeType; - const spec = droppedDataObject[nodeType] as TaskSpec | null; - - return { spec, nodeType }; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/getPositionFromEvent.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/getPositionFromEvent.ts deleted file mode 100644 index 8d674e64ab..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/getPositionFromEvent.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ReactFlowInstance } from "@xyflow/react"; -import type { DragEvent } from "react"; - -export const getPositionFromEvent = ( - event: DragEvent, - reactFlowInstance: ReactFlowInstance, -) => { - let dragOffsetX = 0; - let dragOffsetY = 0; - const dragStartOffsetData = event.dataTransfer.getData("DragStart.offset"); - if (dragStartOffsetData !== "") { - const dragStartOffset = JSON.parse(dragStartOffsetData); - dragOffsetX = dragStartOffset.offsetX ?? 0; - dragOffsetY = dragStartOffset.offsetY ?? 0; - } - - // Node position. Offsets should be included in projection, so that they snap to the grid. - // Otherwise the dropped nodes will be out of phase with the rest of the nodes even when snapping. - const position = reactFlowInstance.screenToFlowPosition({ - x: event.clientX - dragOffsetX, - y: event.clientY - dragOffsetY, - }); - - return position; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/handleAggregatorConnection.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/handleAggregatorConnection.ts deleted file mode 100644 index 42d4b0b4ca..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/handleAggregatorConnection.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { Connection } from "@xyflow/react"; - -import { - AGGREGATOR_ADD_INPUT_HANDLE_ID, - createAggregatorInput, - getNextAggregatorInputName, -} from "@/utils/aggregatorInputs"; -import { isPipelineAggregator } from "@/utils/annotations"; -import type { - ComponentReference, - ComponentSpec, - GraphSpec, - TaskSpec, -} from "@/utils/componentSpec"; -import { isTaskOutputArgument } from "@/utils/componentSpec"; -import { deepClone } from "@/utils/deepClone"; -import { inputNameToNodeId, nodeIdToTaskId } from "@/utils/nodes/nodeIdUtils"; -import { componentSpecToText } from "@/utils/yaml"; - -import { handleConnection } from "./handleConnection"; - -export const handleAggregatorConnection = ( - graphSpec: GraphSpec, - connection: Connection, - updateNodeInternals?: (nodeId: string) => void, -): { - graphSpec: GraphSpec; - shouldCreateEdge: boolean; - redirectedConnection?: Connection; -} => { - const targetHandleId = connection.targetHandle; - - if (targetHandleId !== AGGREGATOR_ADD_INPUT_HANDLE_ID) { - return { - graphSpec: handleConnection(graphSpec, connection), - shouldCreateEdge: false, - }; - } - - const targetTaskId = nodeIdToTaskId(connection.target); - const targetTask = graphSpec.tasks[targetTaskId]; - - if (!targetTask?.componentRef?.spec) { - return { graphSpec, shouldCreateEdge: false }; - } - - const isAggregator = isPipelineAggregator( - targetTask.componentRef.spec.metadata?.annotations, - ); - - if (!isAggregator) { - return { graphSpec, shouldCreateEdge: false }; - } - - // Check if this source is already connected to any agg_* input - const sourceNode = connection.source; - const sourceHandle = connection.sourceHandle; - - // Only check for duplicates if we have valid source information - if (sourceNode) { - const currentArguments = targetTask.arguments || {}; - const aggregatorInputs = Object.entries(currentArguments).filter(([key]) => - key.startsWith("agg_"), - ); - - const isDuplicateSource = aggregatorInputs.some(([, value]) => { - // Check if it's a task output argument - if (isTaskOutputArgument(value)) { - // Only compare if the new connection is also a task output (has sourceHandle) - if (sourceHandle) { - const existingSourceTaskId = value.taskOutput.taskId; - const existingSourceOutputName = value.taskOutput.outputName; - const newSourceTaskId = nodeIdToTaskId(sourceNode); - const newSourceOutputName = sourceHandle.replace(/^output_/, ""); - - return ( - existingSourceTaskId === newSourceTaskId && - existingSourceOutputName === newSourceOutputName - ); - } - } else if (value && typeof value === "object" && "graphInput" in value) { - // It's a graph input argument - // Only compare if the new connection is also a graph input (no sourceHandle) - if (!sourceHandle && sourceNode.startsWith("input_")) { - const existingInputName = value.graphInput.inputName; - const newInputName = sourceNode.replace(/^input_/, ""); - - return existingInputName === newInputName; - } - } - - return false; - }); - - // If this source is already connected, don't add a new input - if (isDuplicateSource) { - console.warn( - "⚠️ This source is already connected to the Input Aggregator. Connection rejected.", - ); - return { graphSpec, shouldCreateEdge: false }; - } - } - - const currentInputs = targetTask.componentRef.spec.inputs || []; - const newInputName = getNextAggregatorInputName(currentInputs); - const newInput = createAggregatorInput(newInputName); - - // Deep clone the spec to ensure this aggregator has an independent copy - const clonedSpec: ComponentSpec = deepClone(targetTask.componentRef.spec); - const updatedInputs = [...(clonedSpec.inputs || []), newInput]; - - // Reconstruct spec with proper key ordering (inputs before outputs) - const orderedSpec: ComponentSpec = { - name: clonedSpec.name, - ...(clonedSpec.description && { description: clonedSpec.description }), - ...(clonedSpec.metadata && { metadata: clonedSpec.metadata }), - inputs: updatedInputs, - ...(clonedSpec.outputs && { outputs: clonedSpec.outputs }), - implementation: clonedSpec.implementation, - }; - - // Update the text field to reflect the new spec - const updatedText = componentSpecToText(orderedSpec); - - const updatedComponentRef: ComponentReference = { - ...targetTask.componentRef, - spec: orderedSpec, - text: updatedText, - }; - - const updatedTask: TaskSpec = { - ...targetTask, - componentRef: updatedComponentRef, - }; - - const graphSpecWithNewInput: GraphSpec = { - ...graphSpec, - tasks: { - ...graphSpec.tasks, - [targetTaskId]: updatedTask, - }, - }; - - // Update node internals immediately if callback is provided - if (updateNodeInternals) { - updateNodeInternals(connection.target); - } - - // Create the redirected connection for React Flow to use - const redirectedConnection: Connection = { - ...connection, - targetHandle: inputNameToNodeId(newInputName), - }; - - return { - graphSpec: graphSpecWithNewInput, - shouldCreateEdge: true, - redirectedConnection, - }; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/handleAggregatorEdgeDeletion.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/handleAggregatorEdgeDeletion.ts deleted file mode 100644 index dd4daab196..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/handleAggregatorEdgeDeletion.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { Edge } from "@xyflow/react"; - -import { isPipelineAggregator } from "@/utils/annotations"; -import type { - ComponentReference, - ComponentSpec, - GraphImplementation, - TaskSpec, -} from "@/utils/componentSpec"; -import { deepClone } from "@/utils/deepClone"; -import { nodeIdToTaskId } from "@/utils/nodes/nodeIdUtils"; -import { componentSpecToText } from "@/utils/yaml"; - -export const handleAggregatorEdgeDeletion = ( - componentSpec: ComponentSpec, - deletedEdges: Edge[], -): ComponentSpec => { - const graphSpec = (componentSpec.implementation as GraphImplementation) - ?.graph; - - if (!graphSpec?.tasks) { - return componentSpec; - } - - let updatedGraphSpec = graphSpec; - - for (const edge of deletedEdges) { - const targetTaskId = nodeIdToTaskId(edge.target); - const targetTask = updatedGraphSpec.tasks[targetTaskId]; - - if (!targetTask?.componentRef?.spec) { - continue; - } - - const isAggregator = isPipelineAggregator( - targetTask.componentRef.spec.metadata?.annotations, - ); - - if (!isAggregator) { - continue; - } - - const targetHandle = edge.targetHandle?.replace(/^input_/, ""); - - if (!targetHandle || !targetHandle.startsWith("agg_")) { - continue; - } - - // Deep clone the spec to ensure this aggregator has an independent copy - const clonedSpec: ComponentSpec = deepClone(targetTask.componentRef.spec); - const filteredInputs = (clonedSpec.inputs || []).filter( - (input) => input.name !== targetHandle, - ); - - // Reconstruct spec with proper key ordering (inputs before outputs) - const orderedSpec: ComponentSpec = { - name: clonedSpec.name, - ...(clonedSpec.description && { description: clonedSpec.description }), - ...(clonedSpec.metadata && { metadata: clonedSpec.metadata }), - inputs: filteredInputs, - ...(clonedSpec.outputs && { outputs: clonedSpec.outputs }), - implementation: clonedSpec.implementation, - }; - - // Update the text field to reflect the new spec - const updatedText = componentSpecToText(orderedSpec); - - const updatedArguments = { ...targetTask.arguments }; - delete updatedArguments[targetHandle]; - - const updatedComponentRef: ComponentReference = { - ...targetTask.componentRef, - spec: orderedSpec, - text: updatedText, - }; - - const updatedTask: TaskSpec = { - ...targetTask, - componentRef: updatedComponentRef, - arguments: updatedArguments, - }; - - updatedGraphSpec = { - ...updatedGraphSpec, - tasks: { - ...updatedGraphSpec.tasks, - [targetTaskId]: updatedTask, - }, - }; - } - - return { - ...componentSpec, - implementation: { - ...componentSpec.implementation, - graph: updatedGraphSpec, - }, - }; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.ts deleted file mode 100644 index fad0e0c97c..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Connection } from "@xyflow/react"; - -import type { - GraphInputArgument, - GraphSpec, - TaskOutputArgument, -} from "@/utils/componentSpec"; -import { - nodeIdToInputName, - nodeIdToOutputName, - nodeIdToTaskId, -} from "@/utils/nodes/nodeIdUtils"; - -import { setGraphOutputValue } from "./setGraphOutputValue"; -import { setTaskArgument } from "./setTaskArgument"; - -export const handleConnection = ( - graphSpec: GraphSpec, - connection: Connection, -) => { - const targetTaskInputName = connection.targetHandle?.replace(/^input_/, ""); - const sourceTaskOutputName = connection.sourceHandle?.replace(/^output_/, ""); - - if (sourceTaskOutputName !== undefined) { - const taskOutputArgument: TaskOutputArgument = { - taskOutput: { - taskId: nodeIdToTaskId(connection.source), - outputName: sourceTaskOutputName, - }, - }; - - if (targetTaskInputName !== undefined) { - return setTaskArgument( - graphSpec, - nodeIdToTaskId(connection.target), - targetTaskInputName, - taskOutputArgument, - ); - } else { - return setGraphOutputValue( - graphSpec, - nodeIdToOutputName(connection.target), - taskOutputArgument, - ); - // TODO: Perhaps propagate type information - } - } else { - const graphInputName = nodeIdToInputName(connection.source); - const graphInputArgument: GraphInputArgument = { - graphInput: { - inputName: graphInputName, - }, - }; - if (targetTaskInputName !== undefined) { - return setTaskArgument( - graphSpec, - nodeIdToTaskId(connection.target), - targetTaskInputName, - graphInputArgument, - ); - } else { - console.error( - "addConnection: Cannot directly connect graph input to graph output: ", - connection, - ); - } - } - - // GraphSpec was not updated (due to an error or other reason) - return graphSpec; -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/removeEdge.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/removeEdge.ts deleted file mode 100644 index 5654807c84..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/removeEdge.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Edge } from "@xyflow/react"; - -import type { ComponentSpec, GraphImplementation } from "@/utils/componentSpec"; -import { nodeIdToOutputName, nodeIdToTaskId } from "@/utils/nodes/nodeIdUtils"; - -import { setGraphOutputValue } from "./setGraphOutputValue"; -import { setTaskArgument } from "./setTaskArgument"; - -export const removeEdge = (edge: Edge, componentSpec: ComponentSpec) => { - const graphSpec = (componentSpec.implementation as GraphImplementation) - ?.graph; - - const inputName = edge.targetHandle?.replace(/^input_/, ""); - - const updatedComponentSpec = { - ...componentSpec, - }; - - if (inputName !== undefined && graphSpec) { - const taskId = nodeIdToTaskId(edge.target); - const newGraphSpec = setTaskArgument(graphSpec, taskId, inputName); - updatedComponentSpec.implementation = { - ...updatedComponentSpec.implementation, - graph: newGraphSpec, - }; - - return updatedComponentSpec; - } else { - const outputName = nodeIdToOutputName(edge.target); - const newGraphSpec = setGraphOutputValue(graphSpec, outputName); - updatedComponentSpec.implementation = { - ...updatedComponentSpec.implementation, - graph: newGraphSpec, - }; - - return updatedComponentSpec; - } -}; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskAnnotationsInGraphSpec.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskAnnotationsInGraphSpec.ts deleted file mode 100644 index 58d0c00103..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskAnnotationsInGraphSpec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Annotations } from "@/types/annotations"; -import type { GraphSpec } from "@/utils/componentSpec"; - -const replaceTaskAnnotationsInGraphSpec = ( - taskId: string, - graphSpec: GraphSpec, - taskAnnotations: Annotations, -) => { - if (!taskAnnotations) { - return graphSpec; - } - - const newGraphSpec: GraphSpec = { - ...graphSpec, - tasks: { - ...graphSpec.tasks, - [taskId]: { - ...graphSpec.tasks[taskId], - annotations: taskAnnotations, - }, - }, - }; - - return newGraphSpec; -}; - -export default replaceTaskAnnotationsInGraphSpec; diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskNode.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskNode.ts deleted file mode 100644 index e0b9cf89f7..0000000000 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskNode.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { - ArgumentType, - ComponentReference, - GraphSpec, - InputSpec, - TaskSpec, -} from "@/utils/componentSpec"; -import { deepClone } from "@/utils/deepClone"; -import { getUniqueTaskName } from "@/utils/unique"; - -export const replaceTaskNode = ( - taskId: string, - newComponentRef: ComponentReference, - graphSpec: GraphSpec, -) => { - const updatedGraphSpec = deepClone(graphSpec); - - const taskName = newComponentRef.spec?.name; - const newTaskInputs = newComponentRef.spec?.inputs; - const newTaskOutputs = newComponentRef.spec?.outputs; - const newTaskId = getUniqueTaskName(graphSpec, taskName); - - const oldTaskId = taskId; - const oldTask = deepClone(updatedGraphSpec.tasks[oldTaskId]) as TaskSpec; - const oldTaskInputs = oldTask.componentRef.spec?.inputs; - - // Migrate the task to the new componentRef - const updatedTask = { - ...oldTask, - }; - - updatedTask.componentRef = newComponentRef; - - // Determine which of the original task's inputs will be removed - const lostInputs: InputSpec[] = []; - - if (oldTaskInputs) { - oldTaskInputs.forEach((input: InputSpec) => { - const inputName = input.name; - - if (newTaskInputs?.some((input: InputSpec) => input.name === inputName)) { - return; - } - - lostInputs.push(input); - }); - } - - // Migrate Inputs - const oldArguments = updatedTask.arguments; - if (oldArguments) { - const updatedArguments = Object.keys(oldArguments) - .filter((arg) => !lostInputs.some((input) => input.name === arg)) - .reduce( - (acc, key) => { - acc[key] = oldArguments[key]; - return acc; - }, - {} as Record, - ); - - updatedTask.arguments = updatedArguments; - } - - // Migrate outputs - Object.values(updatedGraphSpec.tasks).forEach((task) => { - const updatedArguments = { ...task.arguments }; - - Object.entries(updatedArguments).forEach(([inputName, argument]) => { - if ( - typeof argument === "object" && - argument !== null && - "taskOutput" in argument && - argument.taskOutput && - argument.taskOutput.taskId === oldTaskId - ) { - const outputName = argument.taskOutput.outputName; - - if (newTaskOutputs?.some((output) => output.name === outputName)) { - // Update the taskOutput taskId to the new taskId - updatedArguments[inputName] = { - ...argument, - taskOutput: { - ...argument.taskOutput, - taskId: newTaskId, - }, - }; - } else { - // Remove the argument if the new task does not have the same output - delete updatedArguments[inputName]; - } - } - }); - - task.arguments = updatedArguments; - }); - - // Update GraphSpec - delete updatedGraphSpec.tasks[oldTaskId]; - - updatedGraphSpec.tasks = { - ...updatedGraphSpec.tasks, - [newTaskId]: updatedTask, - }; - - return { updatedGraphSpec, lostInputs, newTaskId, updatedTask } as const; -}; diff --git a/src/components/shared/ReactFlow/FlowControls/FlowControls.tsx b/src/components/shared/ReactFlow/FlowControls/FlowControls.tsx deleted file mode 100644 index d5557bf48d..0000000000 --- a/src/components/shared/ReactFlow/FlowControls/FlowControls.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { - ControlButton, - type ControlProps, - Controls, - type ReactFlowProps, -} from "@xyflow/react"; -import { - LockKeyhole, - LockKeyholeOpen, - SquareDashedMousePointerIcon, -} from "lucide-react"; -import { useCallback, useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack } from "@/components/ui/layout"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Spinner } from "@/components/ui/spinner"; -import { Text } from "@/components/ui/typography"; -import { cn } from "@/lib/utils"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { tracking } from "@/utils/tracking"; - -import type { LayoutAlgorithm } from "../FlowCanvas/utils/autolayout"; - -interface FlowControlsProps extends ControlProps { - config: ReactFlowProps; - updateConfig: (config: Partial) => void; - onAutoLayout?: (algorithm: LayoutAlgorithm) => void; - pageType?: "pipeline_editor" | "pipeline_run"; -} - -const LAYOUT_ALGORITHMS: { - value: LayoutAlgorithm; - label: string; - description: string; -}[] = [ - { value: "sugiyama", label: "Sugiyama", description: "Layered" }, - { - value: "sugiyama_centered", - label: "Sugiyama Centered", - description: "Centered", - }, - { value: "digco", label: "Digco", description: "Compact" }, - { value: "dwyer", label: "Dwyer", description: "Organic" }, -]; - -export default function FlowControls({ - config, - updateConfig, - onAutoLayout, - pageType = "pipeline_editor", - ...props -}: FlowControlsProps) { - const { track } = useAnalytics(); - const [multiSelectActive, setMultiSelectActive] = useState(false); - const [lockActive, setLockActive] = useState(!config.nodesDraggable); - const [layoutPopoverOpen, setLayoutPopoverOpen] = useState(false); - const [isLayouting, setIsLayouting] = useState(false); - - const trackTool = (tool: string) => { - track("pipeline_canvas.tool_bar.tool.click", { - tool, - page_type: pageType, - }); - }; - - const onClickMultiSelect = useCallback(() => { - updateConfig({ - selectionOnDrag: !multiSelectActive, - panOnDrag: multiSelectActive, - }); - setMultiSelectActive(!multiSelectActive); - }, [multiSelectActive, updateConfig]); - - const handleLockChange = useCallback(() => { - updateConfig({ - nodesDraggable: lockActive, - }); - setLockActive(!lockActive); - }, [lockActive, updateConfig]); - - const handleLayoutSelect = async (algorithm: LayoutAlgorithm) => { - setIsLayouting(true); - setLayoutPopoverOpen(false); - - // Delay subsequent execution to the next frame to ensure the popover has closed and the loading spinner has rendered - await new Promise((resolve) => requestAnimationFrame(resolve)); - - try { - await onAutoLayout?.(algorithm); - } finally { - setIsLayouting(false); - } - }; - - return ( - trackTool("zoom_in")} - onZoomOut={() => trackTool("zoom_out")} - onFitView={() => trackTool("fit_view")} - > - {!props.showInteractive && ( - - {lockActive ? ( - - ) : ( - - )} - - )} - - - - {onAutoLayout && ( - - - - {isLayouting ? ( - - ) : ( - - )} - - - - - - Auto Layout - - {LAYOUT_ALGORITHMS.map((algo) => ( - handleLayoutSelect(algo.value)} - disabled={isLayouting} - {...tracking("pipeline_canvas.tool_bar.auto_layout_select", { - selected_layout: algo.value, - page_type: pageType, - })} - > - {algo.label} - ({algo.description}) - - ))} - - - - )} - - ); -} diff --git a/src/components/shared/ReactFlow/FlowControls/index.ts b/src/components/shared/ReactFlow/FlowControls/index.ts deleted file mode 100644 index 100971791b..0000000000 --- a/src/components/shared/ReactFlow/FlowControls/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./FlowControls"; diff --git a/src/components/shared/ReactFlow/FlowSidebar/FlowSidebar.tsx b/src/components/shared/ReactFlow/FlowSidebar/FlowSidebar.tsx deleted file mode 100644 index 0cce2bde3c..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/FlowSidebar.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { BlockStack } from "@/components/ui/layout"; -import { VerticalResizeHandle } from "@/components/ui/resize-handle"; -import { BOTTOM_FOOTER_HEIGHT, TOP_NAV_HEIGHT } from "@/utils/constants"; - -import FileActions from "./sections/FileActions"; -import GraphComponents from "./sections/GraphComponents"; -import RunsAndSubmission from "./sections/RunsAndSubmission"; - -const MIN_WIDTH = 220; -const MAX_WIDTH = 400; -const DEFAULT_WIDTH = 256; - -const FlowSidebar = () => { - return ( - - - - - - - - - - ); -}; - -export default FlowSidebar; diff --git a/src/components/shared/ReactFlow/FlowSidebar/components/RecentExecutionsButton.tsx b/src/components/shared/ReactFlow/FlowSidebar/components/RecentExecutionsButton.tsx deleted file mode 100644 index ad24bef63d..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/components/RecentExecutionsButton.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { List } from "lucide-react"; -import type { ComponentProps, ReactNode } from "react"; - -import { PipelineRunsList } from "@/components/shared/PipelineRunDisplay/PipelineRunsList"; -import { usePipelineRuns } from "@/components/shared/PipelineRunDisplay/usePipelineRuns"; -import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper"; -import { Button } from "@/components/ui/button"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Spinner } from "@/components/ui/spinner"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { tracking } from "@/utils/tracking"; - -export const RecentExecutionsButton = withSuspenseWrapper( - ({ - pipelineName, - overviewConfig, - trigger, - ...rest - }: ComponentProps & { - trigger?: ReactNode; - }) => { - const { data: runs } = usePipelineRuns(pipelineName); - const defaultTrigger = ( - - - - ); - - return ( - - - - - - {trigger ?? defaultTrigger} - - - - Recent Pipeline Runs ({runs.length}) - - - - - - - - ); - }, - // skeleton fallback - () => , - // error fallback - () => , -); diff --git a/src/components/shared/ReactFlow/FlowSidebar/index.ts b/src/components/shared/ReactFlow/FlowSidebar/index.ts deleted file mode 100644 index 1e785a312d..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./FlowSidebar"; diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/FileActions.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/FileActions.tsx deleted file mode 100644 index 9267b43577..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/FileActions.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; - -import { InlineStack } from "@/components/ui/layout"; -import { Spinner } from "@/components/ui/spinner"; -import { Text } from "@/components/ui/typography"; -import { useAutoSaveStatus } from "@/providers/AutoSaveProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { getPipelineFile } from "@/services/pipelineService"; -import { formatRelativeTime } from "@/utils/date"; - -import { SidebarSection } from "../components/SidebarSection"; -import { ExportPipelineButton } from "./components/ExportPipelineButton"; -import { ImportPipelineButton } from "./components/ImportPipelineButton"; -import { SavePipelineAsButton } from "./components/SavePipelineAsButton"; -import { SavePipelineButton } from "./components/SavePipelineButton"; - -interface AutoSaveStatusProps { - lastSavedAt: Date | null; -} - -const AutoSaveStatus = ({ lastSavedAt }: AutoSaveStatusProps) => { - const { autoSaveStatus } = useAutoSaveStatus(); - - const statusText = useMemo(() => { - if (autoSaveStatus.isSaving) { - return "Saving..."; - } - if (autoSaveStatus.lastSavedAt || lastSavedAt) { - return `Last saved ${formatRelativeTime( - autoSaveStatus.lastSavedAt ?? lastSavedAt, - )}`; - } - return "All changes saved"; - }, [autoSaveStatus, lastSavedAt]); - - return ( - - {autoSaveStatus.isSaving && } - - {statusText} - - - ); -}; - -const FileActions = () => { - const { componentSpec } = useComponentSpec(); - const [lastSavedAt, setLastSavedAt] = useState(null); - - useEffect(() => { - const fetchLastSaved = async () => { - if (componentSpec?.name) { - const lastSavedPipeline = await getPipelineFile(componentSpec.name); - setLastSavedAt(lastSavedPipeline?.modificationTime ?? null); - } - }; - fetchLastSaved(); - }, [componentSpec?.name]); - - const handleSaveComplete = () => { - setLastSavedAt(new Date()); - }; - - return ( - - - - - - - - - - - ); -}; - -export default FileActions; diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/RunsAndSubmission.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/RunsAndSubmission.tsx deleted file mode 100644 index 546a00cda5..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/RunsAndSubmission.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization"; -import { HuggingFaceAuthButton } from "@/components/shared/HuggingFaceAuth/HuggingFaceAuthButton"; -import GoogleCloudSubmissionDialog from "@/components/shared/Submitters/GoogleCloud/GoogleCloudSubmissionDialog"; -import TangleSubmitter from "@/components/shared/Submitters/Tangle/TangleSubmitter"; -import { BlockStack } from "@/components/ui/layout"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { isFixableIssue } from "@/utils/validations"; - -import { RecentExecutionsButton } from "../components/RecentExecutionsButton"; -import { SidebarSection } from "../components/SidebarSection"; - -const RunsAndSubmission = () => { - const { isAuthorized } = useAwaitAuthorization(); - const { componentSpec, isComponentTreeValid, globalValidationIssues } = - useComponentSpec(); - - const onlyFixableIssues = - globalValidationIssues.filter(isFixableIssue).length === - globalValidationIssues.length; - - const showGoogleSubmitter = - import.meta.env.VITE_ENABLE_GOOGLE_CLOUD_SUBMITTER === "true"; - - const showMoreButton = componentSpec?.name ? ( - - ) : null; - - return ( - - - - {isAuthorized ? ( - - ) : ( - - )} - - - {showGoogleSubmitter && ( - - - - )} - - - ); -}; - -export default RunsAndSubmission; diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/components/ExportPipelineButton.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/components/ExportPipelineButton.tsx index 9fa6dcdecb..a4e7870d24 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/components/ExportPipelineButton.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/sections/components/ExportPipelineButton.tsx @@ -1,36 +1,3 @@ -import { useCallback, useMemo } from "react"; - -import { ActionButton } from "@/components/shared/Buttons/ActionButton"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { tracking } from "@/utils/tracking"; -import { componentSpecToYaml } from "@/utils/yaml"; - -export const ExportPipelineButton = () => { - const { componentSpec } = useComponentSpec(); - - const componentText = useMemo(() => { - try { - return componentSpecToYaml(componentSpec); - } catch (err) { - console.error("Error preparing pipeline for export:", err); - return componentSpec ? componentSpecToYaml(componentSpec) : ""; - } - }, [componentSpec]); - - const handleExport = useCallback(() => { - exportPipeline(componentSpec?.name ?? "Untitled Pipeline", componentText); - }, [componentText, componentSpec?.name]); - - return ( - - ); -}; - export function exportPipeline(name: string, componentSpecYaml: string) { const blob = new Blob([componentSpecYaml], { type: "text/yaml" }); const url = URL.createObjectURL(blob); diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/components/ImportPipelineButton.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/components/ImportPipelineButton.tsx deleted file mode 100644 index 626d13abf0..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/components/ImportPipelineButton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ActionButton } from "@/components/shared/Buttons/ActionButton"; -import ImportPipeline from "@/components/shared/ImportPipeline"; -import { tracking } from "@/utils/tracking"; - -export const ImportPipelineButton = () => { - return ( - {}} - {...tracking("pipeline_editor.pipeline_actions.import_pipeline")} - /> - } - /> - ); -}; diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/components/SavePipelineAsButton.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/components/SavePipelineAsButton.tsx deleted file mode 100644 index 41e0225e8d..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/components/SavePipelineAsButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useNavigate } from "@tanstack/react-router"; -import { useCallback } from "react"; - -import { ActionButton } from "@/components/shared/Buttons/ActionButton"; -import { PipelineNameDialog } from "@/components/shared/Dialogs"; -import useToastNotification from "@/hooks/useToastNotification"; -import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { EDITOR_PATH } from "@/routes/router"; -import { useSavePipeline } from "@/services/pipelineService"; -import { tracking } from "@/utils/tracking"; - -interface SavePipelineAsButtonProps { - onSaveComplete?: (name: string) => void; -} - -export const SavePipelineAsButton = ({ - onSaveComplete, -}: SavePipelineAsButtonProps) => { - const { componentSpec } = useComponentSpec(); - const { savePipeline } = useSavePipeline(componentSpec); - const notify = useToastNotification(); - const { track } = useAnalytics(); - const navigate = useNavigate(); - - const getDuplicatePipelineName = useCallback(() => { - return componentSpec?.name - ? `${componentSpec.name} (Copy)` - : `Untitled Pipeline ${new Date().toLocaleTimeString()}`; - }, [componentSpec?.name]); - - const handleSavePipelineAs = useCallback( - async (name: string) => { - await savePipeline(name); - track("pipeline_editor.pipeline_actions.save_pipeline_as_completed"); - notify(`Pipeline saved as "${name}"`, "success"); - onSaveComplete?.(name); - - navigate({ - to: `${EDITOR_PATH}/${encodeURIComponent(name)}`, - }); - }, - [navigate, savePipeline, notify, onSaveComplete, track], - ); - - return ( - {}} - {...tracking("pipeline_editor.pipeline_actions.save_pipeline_as")} - /> - } - title="Save Pipeline As" - description="Enter a name for your pipeline" - initialName={getDuplicatePipelineName()} - onSubmit={handleSavePipelineAs} - submitButtonText="Save" - /> - ); -}; diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/components/SavePipelineButton.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/components/SavePipelineButton.tsx deleted file mode 100644 index 4923c21c29..0000000000 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/components/SavePipelineButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback } from "react"; - -import { ActionButton } from "@/components/shared/Buttons/ActionButton"; -import useToastNotification from "@/hooks/useToastNotification"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useSavePipeline } from "@/services/pipelineService"; -import { tracking } from "@/utils/tracking"; - -interface SavePipelineButtonProps { - onSaveComplete?: () => void; -} - -export const SavePipelineButton = ({ - onSaveComplete, -}: SavePipelineButtonProps) => { - const { componentSpec } = useComponentSpec(); - const { savePipeline } = useSavePipeline(componentSpec); - const notify = useToastNotification(); - - const handleSavePipeline = useCallback(async () => { - await savePipeline(); - notify( - `Pipeline saved as "${componentSpec?.name ?? "Untitled Pipeline"}"`, - "success", - ); - onSaveComplete?.(); - }, [savePipeline, notify, componentSpec?.name, onSaveComplete]); - - return ( - - ); -}; diff --git a/src/components/shared/ReactFlow/index.ts b/src/components/shared/ReactFlow/index.ts deleted file mode 100644 index 90d54faec2..0000000000 --- a/src/components/shared/ReactFlow/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as FlowCanvas } from "./FlowCanvas"; -export { default as FlowControls } from "./FlowControls"; -export { default as FlowSidebar } from "./FlowSidebar"; diff --git a/src/components/shared/RunVersionToggle.test.tsx b/src/components/shared/RunVersionToggle.test.tsx deleted file mode 100644 index 3abeb8f7a0..0000000000 --- a/src/components/shared/RunVersionToggle.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { - cleanup, - fireEvent, - render, - screen, - waitFor, -} from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { RunVersionToggle } from "./RunVersionToggle"; - -const mocks = vi.hoisted(() => ({ - navigate: vi.fn(), - pathname: "/runs/run-1", - params: { id: "run-1" } as Record, - enabled: true, -})); - -vi.mock("@tanstack/react-router", () => ({ - useLocation: () => ({ pathname: mocks.pathname }), - useNavigate: () => mocks.navigate, - useParams: () => mocks.params, -})); - -vi.mock("@/components/shared/Settings/useFlags", () => ({ - useFlagValue: () => mocks.enabled, -})); - -describe("RunVersionToggle", () => { - beforeEach(() => { - mocks.navigate.mockReset(); - mocks.pathname = "/runs/run-1"; - mocks.params = { id: "run-1" }; - mocks.enabled = true; - localStorage.removeItem("seen-run-v2-welcome"); - }); - - afterEach(cleanup); - - it("switches from the V1 run view to V2", () => { - render(); - - fireEvent.click( - screen.getByRole("button", { name: "Switch to V2 run view" }), - ); - - expect(mocks.navigate).toHaveBeenCalledWith({ to: "/runs-v2/run-1" }); - }); - - it("switches from V2 to V1 while preserving the subgraph execution", () => { - mocks.pathname = "/runs-v2/run-1/subgraph-1"; - mocks.params = { id: "run-1", subgraphExecutionId: "subgraph-1" }; - render(); - - fireEvent.click( - screen.getByRole("button", { name: "Switch to V1 run view" }), - ); - - expect(mocks.navigate).toHaveBeenCalledWith({ - to: "/runs/run-1/subgraph-1", - }); - }); - - it("introduces the version toggle in the V2 run view", async () => { - mocks.pathname = "/runs-v2/run-1"; - render(); - - expect( - await screen.findByRole("dialog", { - name: "Welcome to the new run view", - }), - ).toHaveTextContent( - "You can easily switch between the new and old run views here.", - ); - - fireEvent.click(screen.getByRole("button", { name: "Got it" })); - - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - expect(localStorage.getItem("seen-run-v2-welcome")).toBe("true"); - }); - - it("does not render when the V2 experience flag is disabled", () => { - mocks.enabled = false; - render(); - - expect(screen.queryByRole("button")).not.toBeInTheDocument(); - }); -}); diff --git a/src/components/shared/RunVersionToggle.tsx b/src/components/shared/RunVersionToggle.tsx deleted file mode 100644 index f8522f02f0..0000000000 --- a/src/components/shared/RunVersionToggle.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useLocation, useParams } from "@tanstack/react-router"; - -import { VersionToggle } from "@/components/shared/VersionToggle"; -import { - hasSeenRunV2Welcome, - markRunV2WelcomeSeen, -} from "@/components/shared/WelcomeSpotlight"; -import { APP_ROUTES } from "@/routes/appRoutes"; -import { getRunPath, type RunViewVersion } from "@/routes/runRoutes"; - -function detectRunViewVersion(pathname: string): RunViewVersion | null { - if (pathname.startsWith(`${APP_ROUTES.RUNS_V2}/`)) return "v2"; - if (pathname.startsWith(`${APP_ROUTES.RUNS}/`)) return "v1"; - return null; -} - -interface RunVersionToggleProps { - showWelcomeSpotlight?: boolean; -} - -export function RunVersionToggle({ - showWelcomeSpotlight = false, -}: RunVersionToggleProps) { - const location = useLocation(); - const params = useParams({ strict: false }); - const version = detectRunViewVersion(location.pathname); - const runId = - "id" in params && typeof params.id === "string" ? params.id : undefined; - if (!version || !runId) return null; - - const subgraphExecutionId = - "subgraphExecutionId" in params && - typeof params.subgraphExecutionId === "string" - ? params.subgraphExecutionId - : undefined; - const targetVersion = version === "v1" ? "v2" : "v1"; - const label = `Switch to ${targetVersion.toUpperCase()} run view`; - - return ( - - ); -} diff --git a/src/components/shared/TaskDetails/Actions.tsx b/src/components/shared/TaskDetails/Actions.tsx index 88de44bc33..efbd9f2c7a 100644 --- a/src/components/shared/TaskDetails/Actions.tsx +++ b/src/components/shared/TaskDetails/Actions.tsx @@ -1,5 +1,5 @@ import { useAnalytics } from "@/providers/AnalyticsProvider"; -import { type TaskNodeContextType } from "@/providers/TaskNodeProvider"; +import type { TaskNodeContextType } from "@/types/taskNode"; import type { HydratedComponentReference } from "@/utils/componentSpec"; import { componentMetadata } from "@/utils/componentTracking"; import { isSubgraph } from "@/utils/subgraphUtils"; diff --git a/src/components/shared/TaskDetails/Details.tsx b/src/components/shared/TaskDetails/Details.tsx index 80664eda0a..1e0063ab0a 100644 --- a/src/components/shared/TaskDetails/Details.tsx +++ b/src/components/shared/TaskDetails/Details.tsx @@ -2,7 +2,7 @@ import { type ReactNode } from "react"; import { BlockStack } from "@/components/ui/layout"; import { useGuaranteedHydrateComponentReference } from "@/hooks/useHydrateComponentReference"; -import type { TaskNodeContextType } from "@/providers/TaskNodeProvider"; +import type { TaskNodeContextType } from "@/types/taskNode"; import type { ComponentReference } from "@/utils/componentSpec"; import { getExecutionStatusLabel } from "@/utils/executionStatus"; import { buildComponentSourceUrl } from "@/utils/URL"; diff --git a/src/components/shared/UndoRedo.tsx b/src/components/shared/UndoRedo.tsx deleted file mode 100644 index fbe9a33603..0000000000 --- a/src/components/shared/UndoRedo.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Redo2, Undo2 } from "lucide-react"; -import { useEffect } from "react"; - -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { KEYBOARD_SHORTCUTS } from "@/utils/constants"; - -import TooltipButton from "./Buttons/TooltipButton"; - -export const UndoRedo = () => { - const { undoRedo } = useComponentSpec(); - const { undo, redo, canUndo, canRedo } = undoRedo; - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - const target = event.target as HTMLElement; - const isInputFocused = - target && - (target.tagName === "INPUT" || - target.tagName === "TEXTAREA" || - target.isContentEditable || - target.closest('[data-slot="input"]')); - - // Skip canvas-level undo/redo if an input is focused - if (isInputFocused) { - return; - } - - if (event.ctrlKey || event.metaKey) { - if (event.key === KEYBOARD_SHORTCUTS.UNDO && !event.shiftKey) { - event.preventDefault(); - undo(); - } else if ( - event.key === KEYBOARD_SHORTCUTS.REDO || - (event.key === KEYBOARD_SHORTCUTS.UNDO && event.shiftKey) - ) { - event.preventDefault(); - redo(); - } - } - }; - - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [undo, redo]); - - const metaKey = navigator.userAgent.includes("Mac") - ? KEYBOARD_SHORTCUTS.MAC_META - : KEYBOARD_SHORTCUTS.WINDOWS_META; - - return ( - - - - - - - - - ); -}; diff --git a/src/components/shared/VersionToggle.tsx b/src/components/shared/VersionToggle.tsx deleted file mode 100644 index b4d4b0cc55..0000000000 --- a/src/components/shared/VersionToggle.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useNavigate } from "@tanstack/react-router"; -import { useRef, useState } from "react"; - -import TooltipButton from "@/components/shared/Buttons/TooltipButton"; -import { useFlagValue } from "@/components/shared/Settings/useFlags"; -import { WelcomeSpotlight } from "@/components/shared/WelcomeSpotlight"; -import { Icon } from "@/components/ui/icon"; -import { ExistingFlags } from "@/flags"; -import { cn } from "@/lib/utils"; -import { tracking } from "@/utils/tracking"; - -type Version = "v1" | "v2"; - -interface VersionToggleWelcome { - hasSeen: () => boolean; - markSeen: () => void; - title: string; - description: string; - titleId: string; - dismissLabel: string; -} - -interface VersionToggleProps { - flagName: keyof typeof ExistingFlags; - targetVersion: Version; - targetPath: string; - tooltip: string; - welcome: VersionToggleWelcome; - showWelcomeSpotlight?: boolean; - trackingId?: string; -} - -export function VersionToggle({ - flagName, - targetVersion, - targetPath, - tooltip, - welcome, - showWelcomeSpotlight = false, - trackingId, -}: VersionToggleProps) { - const navigate = useNavigate(); - const isEnabled = useFlagValue(flagName); - const toggleRef = useRef(null); - const [welcomeSeen, setWelcomeSeen] = useState(welcome.hasSeen); - - const dismissWelcome = () => { - welcome.markSeen(); - setWelcomeSeen(true); - }; - - if (!isEnabled) return null; - - const showWelcome = showWelcomeSpotlight && !welcomeSeen; - - return ( - <> - { - if (showWelcome) dismissWelcome(); - navigate({ to: targetPath }); - }} - variant="header" - size="icon" - aria-label={tooltip} - {...(trackingId - ? tracking(trackingId, { target_version: targetVersion }) - : {})} - > - - - {showWelcome && ( - - )} - > - ); -} diff --git a/src/components/shared/WelcomeSpotlight.tsx b/src/components/shared/WelcomeSpotlight.tsx deleted file mode 100644 index c926df1009..0000000000 --- a/src/components/shared/WelcomeSpotlight.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { - type RefObject, - useEffect, - useLayoutEffect, - useRef, - useState, -} from "react"; -import { createPortal } from "react-dom"; - -import { Button } from "@/components/ui/button"; -import { BlockStack } from "@/components/ui/layout"; -import { Paragraph, Text } from "@/components/ui/typography"; -import { getStorage } from "@/utils/typedStorage"; - -interface V2WelcomeStorage { - "seen-editor-v2-welcome": boolean; - "seen-run-v2-welcome": boolean; -} - -const storage = getStorage(); -const EDITOR_STORAGE_KEY = "seen-editor-v2-welcome"; -const RUN_STORAGE_KEY = "seen-run-v2-welcome"; -const SPOTLIGHT_PADDING = 16; -const CARD_WIDTH = 320; -const SPOTLIGHT_COLOR = "#5B35F5"; -const SPOTLIGHT_HALO_COLOR = "rgba(91, 53, 245, 0.35)"; - -interface SpotlightRect { - centerX: number; - centerY: number; - radius: number; -} - -export function hasSeenEditorV2Welcome(): boolean { - return storage.getItem(EDITOR_STORAGE_KEY) === true; -} - -export function markEditorV2WelcomeSeen() { - storage.setItem(EDITOR_STORAGE_KEY, true); -} - -export function hasSeenRunV2Welcome(): boolean { - return storage.getItem(RUN_STORAGE_KEY) === true; -} - -export function markRunV2WelcomeSeen() { - storage.setItem(RUN_STORAGE_KEY, true); -} - -function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max); -} - -function getSpotlightRect(target: HTMLElement): SpotlightRect { - const rect = target.getBoundingClientRect(); - return { - centerX: rect.left + rect.width / 2, - centerY: rect.top + rect.height / 2, - radius: Math.max(rect.width, rect.height) / 2 + SPOTLIGHT_PADDING, - }; -} - -interface ClickBlockersProps { - spotlight: SpotlightRect; - onDismiss: () => void; - dismissLabel: string; -} - -function ClickBlockers({ - spotlight, - onDismiss, - dismissLabel, -}: ClickBlockersProps) { - const { centerX, centerY, radius } = spotlight; - const top = Math.max(centerY - radius, 0); - const bottom = Math.min(centerY + radius, window.innerHeight); - const left = Math.max(centerX - radius, 0); - const right = Math.min(centerX + radius, window.innerWidth); - - return ( - <> - - - - - > - ); -} - -interface WelcomeSpotlightProps { - targetRef: RefObject; - onDismiss: () => void; - title: string; - description: string; - titleId: string; - dismissLabel: string; -} - -export function WelcomeSpotlight({ - targetRef, - onDismiss, - title, - description, - titleId, - dismissLabel, -}: WelcomeSpotlightProps) { - const [spotlight, setSpotlight] = useState(null); - const gotItRef = useRef(null); - - useLayoutEffect(() => { - const update = () => { - if (!targetRef.current) return; - setSpotlight(getSpotlightRect(targetRef.current)); - }; - - update(); - const animationFrame = window.requestAnimationFrame(update); - window.addEventListener("resize", update); - window.addEventListener("scroll", update, true); - - return () => { - window.cancelAnimationFrame(animationFrame); - window.removeEventListener("resize", update); - window.removeEventListener("scroll", update, true); - }; - }, [targetRef]); - - useEffect(() => { - gotItRef.current?.focus(); - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") { - onDismiss(); - return; - } - if (event.key === "Tab") { - event.preventDefault(); - gotItRef.current?.focus(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [onDismiss]); - - if (!spotlight) return null; - - const cardTop = clamp( - spotlight.centerY + spotlight.radius + 16, - 16, - window.innerHeight - 180, - ); - const cardLeft = clamp( - spotlight.centerX - CARD_WIDTH / 2, - 16, - window.innerWidth - CARD_WIDTH - 16, - ); - - return createPortal( - <> - - - - - - - - {title} - - - {description} - - - - Got it - - - - >, - document.body, - ); -} diff --git a/src/favicon.ts b/src/favicon.ts deleted file mode 100644 index 43a12c3082..0000000000 --- a/src/favicon.ts +++ /dev/null @@ -1,32 +0,0 @@ -class FaviconManager { - private defaultFavicon = "/favicon.ico"; - private statusIcons = { - success: "/favicon-success.ico", - failed: "/favicon-failed.ico", - loading: "/favicon-loading.ico", - paused: "/favicon-paused.ico", - }; - - updateFavicon( - status: "success" | "failed" | "loading" | "paused" | "default", - ) { - const link = - (document.querySelector("link[rel*='icon']") as HTMLLinkElement) || - document.createElement("link"); - - link.type = "image/x-icon"; - link.rel = "shortcut icon"; - link.href = - status === "default" ? this.defaultFavicon : this.statusIcons[status]; - - if (!document.querySelector("link[rel*='icon']")) { - document.getElementsByTagName("head")[0].appendChild(link); - } - } - - reset() { - this.updateFavicon("default"); - } -} - -export const faviconManager = new FaviconManager(); diff --git a/src/flags.ts b/src/flags.ts index 48ed753f9d..bb83fb53fd 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -40,14 +40,6 @@ export const ExistingFlags: ConfigFlags = { category: "beta", }, - ["v2_editor"]: { - name: "V2 Editor and Run View", - description: - "Enable the V2 editor and run view. You can switch between the V1 and V2 experiences.", - default: true, - category: "beta", - }, - ["ai-assistant"]: { name: "AI Assistant", description: diff --git a/src/hooks/useAutoSave.ts b/src/hooks/useAutoSave.ts deleted file mode 100644 index f1706875fe..0000000000 --- a/src/hooks/useAutoSave.ts +++ /dev/null @@ -1,79 +0,0 @@ -import equal from "fast-deep-equal"; -import { useEffect, useRef, useState } from "react"; - -import { deepClone } from "@/utils/deepClone"; - -const AUTOSAVE_LOADING_DURATION_MS = 1000; // Minimum duration to show "Saving..." status -export interface AutoSaveStatus { - isSaving: boolean; - lastSavedAt: Date | null; - isDirty: boolean; -} - -export const useAutoSave = ( - saveFunction: (data: T) => Promise | void, - data: T, - debounceMs: number = 2000, - shouldSave: boolean = true, -) => { - const timeoutRef = useRef(null); - const lastSavedDataRef = useRef(deepClone(data)); - const [isSaving, setIsSaving] = useState(false); - const [lastSavedAt, setLastSavedAt] = useState(null); - - const isDirty = !equal(data, lastSavedDataRef.current); - - useEffect(() => { - let isCancelled = false; - - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - if (!shouldSave && isDirty) { - lastSavedDataRef.current = deepClone(data); - return; - } - - if (shouldSave && isDirty) { - timeoutRef.current = setTimeout(async () => { - if (isCancelled) return; - - setIsSaving(true); - - const savePromise = (async () => { - try { - await saveFunction(data); - if (!isCancelled) { - lastSavedDataRef.current = deepClone(data); - setLastSavedAt(new Date()); - } - } catch (error) { - console.error("Auto-save failed:", error); - } - })(); - - const minDisplayPromise = new Promise((resolve) => - setTimeout(resolve, AUTOSAVE_LOADING_DURATION_MS), - ); - - await Promise.all([savePromise, minDisplayPromise]); - - setIsSaving(false); - }, debounceMs); - } - - return () => { - isCancelled = true; - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, [data, isDirty, saveFunction, debounceMs, shouldSave]); - - return { - isSaving, - lastSavedAt, - isDirty, - }; -}; diff --git a/src/hooks/useCheckComponentSpecFromPath.ts b/src/hooks/useCheckComponentSpecFromPath.ts index 956fe55661..a30c77a708 100644 --- a/src/hooks/useCheckComponentSpecFromPath.ts +++ b/src/hooks/useCheckComponentSpecFromPath.ts @@ -3,7 +3,7 @@ import { useMemo } from "react"; import { buildTaskSpecShape } from "@/components/shared/PipelineRunNameTemplate/types"; import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; -import { RUNS_BASE_PATH } from "@/routes/router"; +import { RUNS_BASE_PATH } from "@/routes/appRoutes"; import { loadPipelineByName } from "@/services/pipelineService"; import { extractCanonicalName } from "@/utils/canonicalPipelineName"; import type { ComponentSpec } from "@/utils/componentSpec"; diff --git a/src/hooks/useComponentFromUrl.ts b/src/hooks/useComponentFromUrl.ts deleted file mode 100644 index 2994f8d0e5..0000000000 --- a/src/hooks/useComponentFromUrl.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; - -import { - fetchAndStoreComponentByUrl, - parseComponentData, -} from "@/services/componentService"; -import type { ComponentSpec } from "@/utils/componentSpec"; -import { generateDigest } from "@/utils/componentStore"; -import { getComponentByUrl } from "@/utils/localforage"; - -const useComponentFromUrl = (url?: string) => { - const [isLoading, setIsLoading] = useState(true); - const [componentSpec, setComponentSpec] = useState(); - const [componentText, setComponentText] = useState(); - const [componentDigest, setComponentDigest] = useState(); - const [error, setError] = useState(null); - - useEffect(() => { - const controller = new AbortController(); - const signal = controller.signal; - - const loadComponent = async () => { - if (!url) return; - - try { - setIsLoading(true); - setError(null); - - const storedComponent = await getComponentByUrl(url); - - if (storedComponent) { - if (signal.aborted) return; - // Parse the component data - const text = storedComponent.data; - setComponentText(text); - - try { - // Parse the component spec from the text - const parsedSpec = parseComponentData(text); - if (parsedSpec) { - setComponentSpec(parsedSpec); - const digest = await generateDigest(text); - setComponentDigest(digest); - setIsLoading(false); - return; - } - } catch (err) { - console.error("Error parsing component from local storage:", err); - // Fall through to network fetch - } - } - - // If component doesn't exist in storage or parsing failed, fetch and store it - const spec = await fetchAndStoreComponentByUrl(url); - if (signal.aborted) return; - - if (spec) { - setComponentSpec(spec); - - // Get the stored component to get the text - const updatedComponent = await getComponentByUrl(url); - if (updatedComponent && !signal.aborted) { - const text = updatedComponent.data; - setComponentText(text); - const digest = await generateDigest(text); - setComponentDigest(digest); - } - } else { - throw new Error("Failed to load component specification"); - } - } catch (err) { - if (!signal.aborted) { - setError(err instanceof Error ? err.message : String(err)); - } - } finally { - if (!signal.aborted) { - setIsLoading(false); - } - } - }; - - loadComponent(); - return () => controller.abort(); - }, [url]); - - const componentRef = useMemo( - () => ({ - spec: componentSpec, - digest: componentDigest, - text: componentText, - url: url, - }), - [componentSpec, componentDigest, componentText], - ); - - return { isLoading, error, componentRef }; -}; - -export default useComponentFromUrl; diff --git a/src/hooks/useCopyPaste.ts b/src/hooks/useCopyPaste.ts deleted file mode 100644 index 350bb3f9b6..0000000000 --- a/src/hooks/useCopyPaste.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect } from "react"; - -interface UseCopyPasteProps { - onCopy: () => void; - onPaste: () => void; -} - -export function useCopyPaste({ onCopy, onPaste }: UseCopyPasteProps) { - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; - const copyKey = isMac ? event.metaKey : event.ctrlKey; - - const target = event.target as HTMLElement; - - // Check if the target is editable - const isEditable = - target instanceof HTMLInputElement || - target instanceof HTMLTextAreaElement || - target.isContentEditable || - target.closest(".monaco-editor") !== null; - - // Check if there is a text selection - const selection = window.getSelection(); - const hasSelectionText = - selection && selection.toString().trim().length > 0; - - if (isEditable || hasSelectionText) return; - - if (copyKey && event.key === "c") { - event.preventDefault(); - onCopy(); - } - - if (copyKey && event.key === "v") { - event.preventDefault(); - onPaste(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [onCopy, onPaste]); -} diff --git a/src/hooks/useEdgeSelectionHighlight.ts b/src/hooks/useEdgeSelectionHighlight.ts deleted file mode 100644 index ac04aa1d07..0000000000 --- a/src/hooks/useEdgeSelectionHighlight.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useEdges } from "@xyflow/react"; - -export function useEdgeSelectionHighlight(nodeId: string) { - const edges = useEdges(); - - const selectedEdges = edges.filter((edge) => edge.selected); - const hasAnySelectedEdge = selectedEdges.length > 0; - const isConnectedToSelectedEdge = - hasAnySelectedEdge && - selectedEdges.some( - (edge) => edge.source === nodeId || edge.target === nodeId, - ); - - return { isConnectedToSelectedEdge, hasAnySelectedEdge }; -} diff --git a/src/hooks/useGhostNode.ts b/src/hooks/useGhostNode.ts deleted file mode 100644 index 604b296d03..0000000000 --- a/src/hooks/useGhostNode.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { Node } from "@xyflow/react"; -import { useConnection } from "@xyflow/react"; -import { useMemo } from "react"; - -import type { GhostNodeData } from "@/components/shared/ReactFlow/FlowCanvas/GhostNode/types"; -import { createGhostNode } from "@/components/shared/ReactFlow/FlowCanvas/GhostNode/utils"; -import { - AGGREGATOR_ADD_INPUT_HANDLE_ID, - getNextAggregatorInputName, -} from "@/utils/aggregatorInputs"; -import { isPipelineAggregator } from "@/utils/annotations"; -import type { ComponentSpec } from "@/utils/componentSpec"; -import { isGraphImplementation } from "@/utils/componentSpec"; -import { - nodeIdToInputName, - nodeIdToOutputName, - nodeIdToTaskId, -} from "@/utils/nodes/nodeIdUtils"; - -type UseGhostNodeParams = { - readOnly?: boolean; - active: boolean; - isConnecting: boolean; - implementation: ComponentSpec["implementation"]; -}; - -type UseGhostNodeReturn = { - ghostNode: Node | null; - shouldCreateIONode: boolean; -}; - -type GhostNodeDataExtraction = { - dataType: string; - value?: string; - defaultValue?: string; - connectedTaskLabel?: string; - connectedOutputName?: string; -}; - -/** - * Converts TypeSpecType to a string representation for display. - * Handles both string types and complex object types. - */ -const typeToString = (type: unknown): string => { - if (typeof type === "string") { - return type; - } - if (type && typeof type === "object") { - return JSON.stringify(type); - } - return "Any"; -}; - -/** - * Extracts ghost node data for input connections. - * Returns type information and default values for preview display. - */ -const extractInputGhostData = ( - componentRefSpec: ComponentSpec | undefined, - handleName: string, - taskArguments?: Record, -): GhostNodeDataExtraction => { - const inputSpec = componentRefSpec?.inputs?.find( - (input) => input.name === handleName, - ); - - const taskArgValue = taskArguments?.[handleName]; - const defaultValue = - typeof taskArgValue === "string" ? taskArgValue : inputSpec?.default; - - return { - dataType: typeToString(inputSpec?.type), - defaultValue, - }; -}; - -/** - * Extracts ghost node data for output connections. - * Returns type information, connected task label, and output name for preview display. - */ -const extractOutputGhostData = ( - componentRefSpec: ComponentSpec | undefined, - handleName: string, - taskId: string, -): GhostNodeDataExtraction => { - const outputSpec = componentRefSpec?.outputs?.find( - (output) => output.name === handleName, - ); - return { - dataType: typeToString(outputSpec?.type), - value: handleName, - connectedOutputName: outputSpec?.name ?? handleName, - connectedTaskLabel: componentRefSpec?.name ?? taskId, - }; -}; - -export const useGhostNode = ({ - readOnly, - active, - isConnecting, - implementation, -}: UseGhostNodeParams): UseGhostNodeReturn => { - const connectionPosition = useConnection((connection) => connection.to); - const connectionFromHandle = useConnection( - (connection) => connection.fromHandle, - ); - const connectionFromNode = useConnection((connection) => connection.fromNode); - const connectionToHandle = useConnection((connection) => connection.toHandle); - const connectionIsValid = useConnection((connection) => connection.isValid); - - const isConnectionTemporarilyValid = - connectionIsValid === true && !!connectionToHandle; - - const ghostNode = useMemo | null>(() => { - if ( - readOnly || - !active || - !isConnecting || - !connectionPosition || - !connectionFromHandle || - connectionFromNode?.type !== "task" || - !connectionFromHandle.type || - !isGraphImplementation(implementation) || - isConnectionTemporarilyValid - ) { - return null; - } - - const taskId = nodeIdToTaskId(connectionFromHandle.nodeId); - const taskSpec = implementation.graph.tasks[taskId]; - const componentRefSpec = taskSpec?.componentRef?.spec; - - const isInputConnection = connectionFromHandle.type === "target"; - const ioType: GhostNodeData["ioType"] = isInputConnection - ? "input" - : "output"; - - const rawHandleId = connectionFromHandle.id ?? ""; - const isAggregatorAddHandle = - isInputConnection && - rawHandleId === AGGREGATOR_ADD_INPUT_HANDLE_ID && - isPipelineAggregator(componentRefSpec?.metadata?.annotations); - - const handleName = isAggregatorAddHandle - ? getNextAggregatorInputName(componentRefSpec?.inputs ?? []) - : isInputConnection - ? nodeIdToInputName(rawHandleId) - : nodeIdToOutputName(rawHandleId); - - const extractedData = isInputConnection - ? extractInputGhostData(componentRefSpec, handleName, taskSpec?.arguments) - : extractOutputGhostData(componentRefSpec, handleName, taskId); - - return createGhostNode({ - position: connectionPosition, - ioType, - label: handleName, - ...extractedData, - }); - }, [ - readOnly, - active, - isConnecting, - connectionPosition, - connectionFromHandle, - implementation, - isConnectionTemporarilyValid, - ]); - - return { - ghostNode, - shouldCreateIONode: !!ghostNode, - }; -}; diff --git a/src/hooks/useHandleEdgeSelection.ts b/src/hooks/useHandleEdgeSelection.ts deleted file mode 100644 index cec95f39b2..0000000000 --- a/src/hooks/useHandleEdgeSelection.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useEdges, useReactFlow } from "@xyflow/react"; - -export function useHandleEdgeSelection(nodeId: string) { - const edges = useEdges(); - const { setEdges } = useReactFlow(); - - const selectEdgesForHandle = ( - handleId: string, - handleType: "source" | "target", - ) => { - const connectedEdgeIds = new Set(); - - for (const edge of edges) { - const isMatch = - handleType === "source" - ? edge.source === nodeId && edge.sourceHandle === handleId - : edge.target === nodeId && edge.targetHandle === handleId; - - if (isMatch) connectedEdgeIds.add(edge.id); - } - - if (connectedEdgeIds.size === 0) return; - - setEdges((eds) => - eds.map((edge) => ({ - ...edge, - selected: connectedEdgeIds.has(edge.id), - })), - ); - }; - - const clearEdgeSelection = () => { - setEdges((eds) => - eds.map((edge) => ({ - ...edge, - selected: false, - })), - ); - }; - - return { selectEdgesForHandle, clearEdgeSelection }; -} diff --git a/src/hooks/useIsMultiSelect.ts b/src/hooks/useIsMultiSelect.ts deleted file mode 100644 index 5ef6058d5a..0000000000 --- a/src/hooks/useIsMultiSelect.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useStore } from "@xyflow/react"; -import { useRef } from "react"; - -export function useIsMultiSelect() { - const isMultiSelect = useStore( - (s) => s.nodes.filter((n) => n.selected).length > 1, - ); - const ref = useRef(isMultiSelect); - ref.current = isMultiSelect; - return { isMultiSelect, isMultiSelectRef: ref }; -} diff --git a/src/hooks/useLoadComponentSpecFromPath.ts b/src/hooks/useLoadComponentSpecFromPath.ts deleted file mode 100644 index b22343773f..0000000000 --- a/src/hooks/useLoadComponentSpecFromPath.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { useLocation } from "@tanstack/react-router"; -import { useEffect, useMemo, useState } from "react"; - -import { useBackend } from "@/providers/BackendProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { RUNS_BASE_PATH } from "@/routes/router"; -import { fetchExecutionDetails } from "@/services/executionService"; -import { loadPipelineByName } from "@/services/pipelineService"; -import type { ComponentReferenceWithSpec } from "@/utils/componentStore"; -import { prepareComponentRefForEditor } from "@/utils/prepareComponentRefForEditor"; -import { getIdOrTitleFromPath } from "@/utils/URL"; - -export const useLoadComponentSpecFromPath = () => { - const location = useLocation(); - - const { backendUrl } = useBackend(); - const { setComponentSpec, clearComponentSpec, componentSpec } = - useComponentSpec(); - - const [isLoadingPipeline, setIsLoadingPipeline] = useState(false); - const [error, setError] = useState(null); - - const pathname = useMemo(() => location.pathname, [location.pathname]); - const isRunPath = pathname.includes(RUNS_BASE_PATH); - - const { title, id } = useMemo( - () => getIdOrTitleFromPath(pathname), - [pathname], - ); - - useEffect(() => { - const loadPipelineFromStorage = async () => { - setError(null); - - if (!title && !id) { - clearComponentSpec(); - return; - } - - setIsLoadingPipeline(true); - try { - // load by run id - if (id && isRunPath) { - const result = await fetchExecutionDetails(String(id), backendUrl); - - if (result.task_spec?.componentRef?.spec) { - const preparedComponentRef = await prepareComponentRefForEditor( - result.task_spec.componentRef as ComponentReferenceWithSpec, - ); - if (preparedComponentRef) { - setComponentSpec(preparedComponentRef); - return; - } - } - } - - // load by title - if (title && !isRunPath) { - const result = await loadPipelineByName(title); - - if (result.experiment?.componentRef?.spec) { - const preparedComponentRef = await prepareComponentRefForEditor( - result.experiment.componentRef as ComponentReferenceWithSpec, - ); - if (preparedComponentRef) { - setComponentSpec(preparedComponentRef); - return; - } - } - } - - setError("No component spec found for the current path."); - clearComponentSpec(); - } catch (error) { - console.error("Error loading pipeline from storage:", error); - if (error instanceof Error) { - setError("Failed to load pipeline from storage: " + error.message); - } - } finally { - setIsLoadingPipeline(false); - } - }; - - loadPipelineFromStorage(); - return () => { - clearComponentSpec(); - }; - }, [id, title, backendUrl, isRunPath, setComponentSpec, clearComponentSpec]); - - return { - componentSpec, - isLoading: isLoadingPipeline, - enableApi: isRunPath, - error, - }; -}; diff --git a/src/hooks/useNodeCallbacks.ts b/src/hooks/useNodeCallbacks.ts deleted file mode 100644 index d157e43629..0000000000 --- a/src/hooks/useNodeCallbacks.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { type Node, useReactFlow } from "@xyflow/react"; -import { useCallback } from "react"; - -import { getDeleteConfirmationDetails } from "@/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/DeleteConfirmation"; -import { getUpgradeConfirmationDetails } from "@/components/shared/ReactFlow/FlowCanvas/ConfirmationDialogs/UpgradeComponent"; -import type { NodesAndEdges } from "@/components/shared/ReactFlow/FlowCanvas/types"; -import { duplicateNodes } from "@/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes"; -import replaceTaskAnnotationsInGraphSpec from "@/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskAnnotationsInGraphSpec"; -import replaceTaskArgumentsInGraphSpec from "@/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskArgumentsInGraphSpec"; -import { replaceTaskNode } from "@/components/shared/ReactFlow/FlowCanvas/utils/replaceTaskNode"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import type { Annotations } from "@/types/annotations"; -import type { NodeAndTaskId } from "@/types/taskNode"; -import type { ComponentReference, TaskSpec } from "@/utils/componentSpec"; -import type { ArgumentType } from "@/utils/componentSpec"; -import { updateSubgraphSpec } from "@/utils/subgraphUtils"; - -import type { TriggerDialogProps } from "./useConfirmationDialog"; -import useToastNotification from "./useToastNotification"; - -interface UseNodeCallbacksProps { - triggerConfirmation: (data: TriggerDialogProps) => Promise; - onElementsRemove: (params: NodesAndEdges) => void; - updateOrAddNodes: (params: { - updatedNodes?: Node[]; - newNodes?: Node[]; - }) => void; -} - -export const useNodeCallbacks = ({ - triggerConfirmation, - onElementsRemove, - updateOrAddNodes, -}: UseNodeCallbacksProps) => { - const notify = useToastNotification(); - const reactFlowInstance = useReactFlow(); - - const { - currentGraphSpec, - currentSubgraphSpec, - currentSubgraphPath, - updateGraphSpec, - componentSpec, - setComponentSpec, - } = useComponentSpec(); - - // Workaround for nodes state being stale in task node callbacks - const getNodeById = useCallback( - (id: string) => { - if (!reactFlowInstance) { - console.warn("React Flow instance is not available."); - return undefined; - } - - const { getNodes } = reactFlowInstance; - const nodes = getNodes(); - if (!nodes) { - console.warn("No nodes found in the current React Flow instance."); - return undefined; - } - - const node = nodes.find((n) => n.id === id); - if (!node) { - console.warn(`Node with id ${id} not found.`); - return undefined; - } - return node; - }, - [reactFlowInstance], - ); - - const onDelete = useCallback( - async (ids: NodeAndTaskId) => { - const nodeId = ids.nodeId; - - const nodeToDelete = getNodeById(nodeId); - - if (!nodeToDelete) { - console.warn(`Node with id ${nodeId} not found.`); - return; - } - - if (!reactFlowInstance) { - console.warn("React Flow instance is not available."); - return; - } - - const currentEdges = reactFlowInstance.getEdges(); - - const edgesToRemove = currentEdges.filter( - (edge) => edge.source === nodeId || edge.target === nodeId, - ); - - const params = { - nodes: [nodeToDelete], - edges: edgesToRemove, - } as NodesAndEdges; - - const confirmed = await triggerConfirmation( - getDeleteConfirmationDetails(params), - ); - - if (confirmed) { - onElementsRemove(params); - } - }, - [triggerConfirmation, onElementsRemove, getNodeById], - ); - - const setArguments = useCallback( - (ids: NodeAndTaskId, args: Record) => { - const taskId = ids.taskId; - const newGraphSpec = replaceTaskArgumentsInGraphSpec( - taskId, - currentGraphSpec, - args, - ); - updateGraphSpec(newGraphSpec); - }, - [currentGraphSpec, updateGraphSpec], - ); - - const setAnnotations = useCallback( - (ids: NodeAndTaskId, annotations: Annotations) => { - const taskId = ids.taskId; - const newGraphSpec = replaceTaskAnnotationsInGraphSpec( - taskId, - currentGraphSpec, - annotations, - ); - updateGraphSpec(newGraphSpec); - }, - [currentGraphSpec, updateGraphSpec], - ); - - const setCacheStaleness = useCallback( - (ids: NodeAndTaskId, cacheStaleness: string | undefined) => { - const taskId = ids.taskId; - const task = currentGraphSpec.tasks[taskId]; - - if (!task) { - console.warn(`Task with id ${taskId} not found in graph spec.`); - return; - } - - const cachingStrategy = cacheStaleness - ? { maxCacheStaleness: cacheStaleness } - : undefined; - - const updatedTask: TaskSpec = { - ...task, - executionOptions: { - ...task.executionOptions, - cachingStrategy, - }, - }; - - const newGraphSpec = { - ...currentGraphSpec, - tasks: { - ...currentGraphSpec.tasks, - [taskId]: updatedTask, - }, - }; - - updateGraphSpec(newGraphSpec); - }, - [currentGraphSpec, updateGraphSpec], - ); - - const onDuplicate = useCallback( - (ids: NodeAndTaskId, selected = true) => { - const nodeId = ids.nodeId; - const node = getNodeById(nodeId); - - if (!node) { - console.warn(`Node with id ${nodeId} not found.`); - return; - } - - const { - updatedComponentSpec: updatedSubgraphSpec, - newNodes, - updatedNodes, - } = duplicateNodes(currentSubgraphSpec, [node], { selected }); - - const updatedRootSpec = updateSubgraphSpec( - componentSpec, - currentSubgraphPath, - updatedSubgraphSpec, - ); - - setComponentSpec(updatedRootSpec); - - updateOrAddNodes({ - updatedNodes, - newNodes, - }); - }, - [ - componentSpec, - currentSubgraphSpec, - currentSubgraphPath, - getNodeById, - setComponentSpec, - updateOrAddNodes, - ], - ); - - const onUpgrade = useCallback( - async (ids: NodeAndTaskId, newComponentRef: ComponentReference) => { - const nodeId = ids.nodeId; - const node = getNodeById(nodeId); - - if (!node) { - console.warn(`Node with id ${nodeId} not found.`); - return; - } - - const { updatedGraphSpec, lostInputs } = replaceTaskNode( - node.data.taskId as string, - newComponentRef, - currentGraphSpec, - ); - - if (!newComponentRef.digest) { - console.error("Component reference does not have a digest."); - return; - } - - const dialogData = getUpgradeConfirmationDetails( - node.data.taskId as string, - node.data.taskSpec as TaskSpec | undefined, - newComponentRef.digest, - lostInputs, - ); - - const confirmed = await triggerConfirmation(dialogData); - - if (confirmed) { - updateGraphSpec(updatedGraphSpec); - notify("Component updated", "success"); - } - }, - [ - currentGraphSpec, - getNodeById, - updateGraphSpec, - triggerConfirmation, - notify, - ], - ); - - const onSelect = useCallback( - (ids: NodeAndTaskId) => { - reactFlowInstance.setNodes((nodes) => - nodes.map((node) => - node.id === ids.nodeId ? { ...node, selected: true } : node, - ), - ); - }, - [reactFlowInstance], - ); - - return { - onDelete, - setArguments, - setAnnotations, - setCacheStaleness, - onDuplicate, - onUpgrade, - onSelect, - }; -}; diff --git a/src/hooks/useRecentlyViewed.ts b/src/hooks/useRecentlyViewed.ts index 0e8a808f9d..0cd597c4f0 100644 --- a/src/hooks/useRecentlyViewed.ts +++ b/src/hooks/useRecentlyViewed.ts @@ -91,7 +91,3 @@ export function addRecentlyViewed(item: Omit) { export function useRecentlyUsed() { return { recentlyUsed: useRecent(RECENTLY_USED_KEY) }; } - -export function addRecentlyUsed(item: Omit) { - addRecent(RECENTLY_USED_KEY, item); -} diff --git a/src/hooks/useSubgraphKeyboardNavigation.ts b/src/hooks/useSubgraphKeyboardNavigation.ts deleted file mode 100644 index 7d50be44c6..0000000000 --- a/src/hooks/useSubgraphKeyboardNavigation.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect } from "react"; - -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; - -/** - * Hook to handle keyboard navigation for subgraphs - * - Escape: Navigate back to parent subgraph - */ -export const useSubgraphKeyboardNavigation = () => { - const { canNavigateBack, navigateBack } = useComponentSpec(); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (!canNavigateBack) return; - - // Don't interfere with typing in input fields, textareas, or contenteditable elements - const activeElement = document.activeElement; - if ( - activeElement instanceof HTMLInputElement || - activeElement instanceof HTMLTextAreaElement || - activeElement?.getAttribute("contenteditable") === "true" - ) { - return; - } - - if (event.key === "Escape") { - event.preventDefault(); - navigateBack(); - } - }; - - document.addEventListener("keydown", handleKeyDown); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; - }, [canNavigateBack, navigateBack]); -}; diff --git a/src/hooks/useTaskNodeDimensions.ts b/src/hooks/useTaskNodeDimensions.ts deleted file mode 100644 index fc2484a7dd..0000000000 --- a/src/hooks/useTaskNodeDimensions.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useMemo } from "react"; - -import type { TaskNodeDimensions } from "@/types/taskNode"; -import { - EDITOR_POSITION_ANNOTATION, - getAnnotationValue, -} from "@/utils/annotations"; -import type { TaskSpec } from "@/utils/componentSpec"; -import { DEFAULT_NODE_DIMENSIONS } from "@/utils/constants"; - -const MIN_WIDTH = 150; -const MIN_HEIGHT = 100; - -type EditorPosition = { - x?: string; - y?: string; - width?: string; - height?: string; - w?: string; - h?: string; -}; - -export function useTaskNodeDimensions(taskSpec?: TaskSpec): TaskNodeDimensions { - return useMemo(() => { - if (!taskSpec) { - return DEFAULT_NODE_DIMENSIONS; - } - - let annotatedDimensions; - try { - const parsed = JSON.parse( - getAnnotationValue( - taskSpec.annotations, - EDITOR_POSITION_ANNOTATION, - '{"x":0,"y":0,"width":150,"height":100}', - ), - ) as EditorPosition | undefined; - - if (parsed) { - const width = parsed.width ?? parsed.w; - const height = parsed.height ?? parsed.h; - - annotatedDimensions = { - x: !isNaN(Number(parsed.x)) ? parsed.x : undefined, - y: !isNaN(Number(parsed.y)) ? parsed.y : undefined, - width: !isNaN(Number(width)) ? width : undefined, - height: !isNaN(Number(height)) ? height : undefined, - }; - } else { - annotatedDimensions = undefined; - } - } catch { - annotatedDimensions = undefined; - } - return annotatedDimensions - ? { - w: Math.max( - parseInt(annotatedDimensions.width ?? "") || - DEFAULT_NODE_DIMENSIONS.w, - MIN_WIDTH, - ), - h: - Math.max(parseInt(annotatedDimensions.height ?? ""), MIN_HEIGHT) || - DEFAULT_NODE_DIMENSIONS.h, - } - : DEFAULT_NODE_DIMENSIONS; - }, [taskSpec]); -} diff --git a/src/providers/AutoSaveProvider.tsx b/src/providers/AutoSaveProvider.tsx deleted file mode 100644 index f1900895a5..0000000000 --- a/src/providers/AutoSaveProvider.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import equal from "fast-deep-equal"; -import { type ReactNode, useEffect, useState } from "react"; - -import { type AutoSaveStatus, useAutoSave } from "@/hooks/useAutoSave"; -import { - createRequiredContext, - useRequiredContext, -} from "@/hooks/useRequiredContext"; -import { - EMPTY_GRAPH_COMPONENT_SPEC, - useComponentSpec, -} from "@/providers/ComponentSpecProvider"; -import { AUTOSAVE_DEBOUNCE_TIME_MS } from "@/utils/constants"; - -interface AutoSaveContextType { - autoSaveStatus: AutoSaveStatus; -} - -const AutoSaveContext = - createRequiredContext("AutosaveProvider"); - -export const AutoSaveProvider = ({ children }: { children: ReactNode }) => { - const { componentSpec, saveComponentSpec, isLoading } = useComponentSpec(); - - const [isEmptySpec, setIsEmptySpec] = useState( - equal(componentSpec, EMPTY_GRAPH_COMPONENT_SPEC), - ); - - const shouldAutoSave = - !isLoading && componentSpec && !!componentSpec.name && !isEmptySpec; - - useEffect(() => { - setIsEmptySpec(equal(componentSpec, EMPTY_GRAPH_COMPONENT_SPEC)); - }, [componentSpec]); - - const autoSaveStatus = useAutoSave( - async (spec) => { - if (spec.name) { - saveComponentSpec(spec.name); - } - }, - componentSpec, - AUTOSAVE_DEBOUNCE_TIME_MS, - shouldAutoSave, - ); - - const value = { autoSaveStatus }; - - return ( - - {children} - - ); -}; - -export const useAutoSaveStatus = () => { - return useRequiredContext(AutoSaveContext); -}; diff --git a/src/providers/ComponentLibraryProvider/ForcedSearchProvider.tsx b/src/providers/ComponentLibraryProvider/ForcedSearchProvider.tsx deleted file mode 100644 index 8c26c4addc..0000000000 --- a/src/providers/ComponentLibraryProvider/ForcedSearchProvider.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - createContext, - type PropsWithChildren, - useCallback, - useContext, - useMemo, - useReducer, -} from "react"; - -import { DEFAULT_FILTERS } from "@/utils/constants"; - -import type { LibraryFilterRequest } from "./types"; - -interface ForcedSearchContextType { - currentSearchFilter: Required; - highlightSearchResults: boolean; -} - -type ForcedSearchAction = - | { type: "updateSearchFilter"; payload: Partial } - | { type: "highlightSearchFilter"; payload: Partial } - | { type: "resetSearchFilter" }; - -// todo: make generic and re-use -type Payload = - Extract extends { payload: infer P } - ? P - : never; - -const ForcedComponentSearchContext = createContext< - ForcedSearchContextType & { - updateSearchFilter: (payload: Payload<"updateSearchFilter">) => void; - highlightSearchFilter: (payload: Payload<"highlightSearchFilter">) => void; - resetSearchFilter: () => void; - } ->({ - currentSearchFilter: { - searchTerm: "", - filters: DEFAULT_FILTERS, - }, - highlightSearchResults: false, - updateSearchFilter: () => {}, - highlightSearchFilter: () => {}, - resetSearchFilter: () => {}, -}); - -const forcedSearchReducer = ( - state: ForcedSearchContextType, - action: ForcedSearchAction, -) => { - switch (action.type) { - case "updateSearchFilter": - return { - ...state, - currentSearchFilter: { - ...state.currentSearchFilter, - ...action.payload, - }, - }; - case "highlightSearchFilter": - return { - ...state, - highlightSearchResults: true, - currentSearchFilter: { - ...state.currentSearchFilter, - ...action.payload, - }, - }; - case "resetSearchFilter": - return { - currentSearchFilter: { - searchTerm: "", - filters: DEFAULT_FILTERS, - }, - highlightSearchResults: false, - }; - } -}; - -export const ForcedSearchProvider = ({ children }: PropsWithChildren) => { - const [state, dispatch] = useReducer(forcedSearchReducer, { - currentSearchFilter: { - searchTerm: "", - filters: DEFAULT_FILTERS, - }, - highlightSearchResults: false, - }); - - const updateSearchFilter = useCallback( - (payload: Partial) => { - dispatch({ type: "updateSearchFilter", payload }); - }, - [], - ); - - const resetSearchFilter = useCallback(() => { - dispatch({ type: "resetSearchFilter" }); - }, []); - - const highlightSearchFilter = useCallback( - (payload: Partial) => { - dispatch({ type: "highlightSearchFilter", payload }); - }, - [], - ); - - const value = useMemo( - () => ({ - ...state, - updateSearchFilter, - resetSearchFilter, - highlightSearchFilter, - }), - [state, updateSearchFilter, resetSearchFilter, highlightSearchFilter], - ); - - return ( - - {children} - - ); -}; - -export const useForcedSearchContext = () => { - return useContext(ForcedComponentSearchContext); -}; diff --git a/src/providers/ComponentSpecProvider.tsx b/src/providers/ComponentSpecProvider.tsx index 34e52518c7..3ba4d07bf2 100644 --- a/src/providers/ComponentSpecProvider.tsx +++ b/src/providers/ComponentSpecProvider.tsx @@ -44,7 +44,7 @@ const EMPTY_GRAPH_SPEC: GraphSpec = { tasks: {}, }; -export const EMPTY_GRAPH_COMPONENT_SPEC: ComponentSpec = { +const EMPTY_GRAPH_COMPONENT_SPEC: ComponentSpec = { implementation: { graph: EMPTY_GRAPH_SPEC, }, diff --git a/src/providers/TaskNodeProvider.tsx b/src/providers/TaskNodeProvider.tsx deleted file mode 100644 index 6600f9d275..0000000000 --- a/src/providers/TaskNodeProvider.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { type ReactNode, useCallback, useMemo } from "react"; - -import useComponentFromUrl from "@/hooks/useComponentFromUrl"; -import { useHydrateComponentReference } from "@/hooks/useHydrateComponentReference"; -import { useTaskNodeDimensions } from "@/hooks/useTaskNodeDimensions"; -import useToastNotification from "@/hooks/useToastNotification"; -import type { Annotations } from "@/types/annotations"; -import { - DEFAULT_TASK_NODE_CALLBACKS, - type TaskNodeData, - type TaskNodeDimensions, -} from "@/types/taskNode"; -import { - EDITOR_COLLAPSED_ANNOTATION, - getAnnotationValue, - removeAnnotation, - setAnnotation, -} from "@/utils/annotations"; -import type { - ArgumentType, - ComponentReference, - HydratedComponentReference, - InputSpec, - OutputSpec, - TaskSpec, -} from "@/utils/componentSpec"; -import { getComponentName, getTaskDisplayName } from "@/utils/getComponentName"; -import { taskIdToNodeId } from "@/utils/nodes/nodeIdUtils"; - -import { - createRequiredContext, - useRequiredContext, -} from "../hooks/useRequiredContext"; - -const EMPTY_COMPONENT_REF: Partial = {}; -const EMPTY_INPUTS: InputSpec[] = []; -const EMPTY_OUTPUTS: OutputSpec[] = []; - -type TaskNodeState = Readonly<{ - selected: boolean; - highlighted: boolean; - readOnly: boolean; - disabled: boolean; - connectable: boolean; - status?: string; - isCustomComponent: boolean; - isCollapsed: boolean; - dimensions: TaskNodeDimensions; -}>; - -type TaskNodeCallbacks = { - setArguments: (args: Record) => void; - setAnnotations: (annotations: Annotations) => void; - setCacheStaleness: (cacheStaleness: string | undefined) => void; - setCollapsed: (collapsed: boolean) => void; - onDelete?: () => void; - onDuplicate?: () => void; - onUpgrade?: () => void; - onSelect?: () => void; -}; - -type TaskNodeProviderProps = { - children: ReactNode; - data: TaskNodeData; - selected: boolean; - status?: string; -}; - -export type TaskNodeContextType = { - componentRef?: HydratedComponentReference; - taskSpec?: TaskSpec; - taskId?: string; - nodeId: string; - inputs: InputSpec[]; - outputs: OutputSpec[]; - name: string; - displayName: string; - state: TaskNodeState; - callbacks: TaskNodeCallbacks; -}; - -const TaskNodeContext = - createRequiredContext("TaskNodeProvider"); - -export const TaskNodeProvider = ({ - children, - data, - selected, - status, -}: TaskNodeProviderProps) => { - const notify = useToastNotification(); - - const taskSpec = data.taskSpec; - const taskId = data.taskId; - const nodeId = taskId ? taskIdToNodeId(taskId) : ""; - - const { - onDelete, - onDuplicate, - onUpgrade, - setArguments, - setAnnotations, - setCacheStaleness, - onSelect, - } = data.callbacks ?? DEFAULT_TASK_NODE_CALLBACKS; - - const componentRef = - useHydrateComponentReference( - taskSpec?.componentRef ?? EMPTY_COMPONENT_REF, - ) || undefined; - - const inputs = componentRef?.spec?.inputs ?? EMPTY_INPUTS; - const outputs = componentRef?.spec?.outputs ?? EMPTY_OUTPUTS; - - const name = getComponentName(componentRef ?? EMPTY_COMPONENT_REF); - const displayName = getTaskDisplayName(taskId ?? "Task", taskSpec); - - const isCustomComponent = !componentRef?.url; // Custom components don't have a source url - - const { componentRef: mostRecentComponentRef } = useComponentFromUrl( - componentRef?.url, - ); - - const isOutdated = componentRef?.digest !== mostRecentComponentRef.digest; - - const dimensions = useTaskNodeDimensions(taskSpec); - - const handleSetArguments = useCallback( - (args: Record) => { - setArguments(args); - }, - [setArguments], - ); - - const handleSetAnnotations = useCallback( - (annotations: Annotations) => { - setAnnotations(annotations); - }, - [setAnnotations], - ); - - const handleSetCacheStaleness = useCallback( - (cacheStaleness: string | undefined) => { - setCacheStaleness(cacheStaleness); - }, - [setCacheStaleness, notify], - ); - - const isCollapsed = - getAnnotationValue(taskSpec?.annotations, EDITOR_COLLAPSED_ANNOTATION) === - "true"; - - const handleSetCollapsed = useCallback( - (collapsed: boolean) => { - const updatedAnnotations = collapsed - ? setAnnotation( - taskSpec?.annotations, - EDITOR_COLLAPSED_ANNOTATION, - "true", - ) - : removeAnnotation(taskSpec?.annotations, EDITOR_COLLAPSED_ANNOTATION); - - setAnnotations(updatedAnnotations ?? {}); - }, - [taskSpec?.annotations, setAnnotations], - ); - - const handleDeleteTaskNode = useCallback(() => { - onDelete(); - }, [onDelete]); - - const handleDuplicateTaskNode = useCallback(() => { - onDuplicate(); - }, [onDuplicate]); - - const handleUpgradeTaskNode = useCallback(() => { - if (!isOutdated) { - notify("Component version already matches source URL", "info"); - return; - } - - onUpgrade(mostRecentComponentRef); - }, [onUpgrade, isOutdated, mostRecentComponentRef, notify]); - - const state = useMemo( - (): TaskNodeState => ({ - selected: selected && !data.isGhost, - highlighted: !!data.highlighted && !data.isGhost, - readOnly: !!data.readOnly || !!data.isGhost, - connectable: !!data.connectable, - status: data.isGhost ? undefined : status, - disabled: data.isGhost ?? false, - isCustomComponent, - isCollapsed, - dimensions, - }), - [ - selected, - data.highlighted, - data.readOnly, - data.isGhost, - status, - isCustomComponent, - isCollapsed, - dimensions, - ], - ); - - const callbacks = useMemo( - () => ({ - setArguments: handleSetArguments, - setAnnotations: handleSetAnnotations, - setCacheStaleness: handleSetCacheStaleness, - setCollapsed: handleSetCollapsed, - onDelete: handleDeleteTaskNode, - onDuplicate: handleDuplicateTaskNode, - onUpgrade: handleUpgradeTaskNode, - onSelect, - }), - [ - handleSetArguments, - handleSetAnnotations, - handleSetCollapsed, - handleDeleteTaskNode, - handleDuplicateTaskNode, - handleUpgradeTaskNode, - onSelect, - ], - ); - - const value = useMemo( - () => ({ - componentRef, - taskSpec, - taskId, - nodeId, - inputs, - outputs, - name, - displayName, - state, - callbacks, - }), - [ - componentRef, - taskSpec, - taskId, - nodeId, - inputs, - outputs, - name, - displayName, - state, - callbacks, - ], - ); - - return ( - - {children} - - ); -}; - -export function useTaskNode() { - return useRequiredContext(TaskNodeContext); -} diff --git a/src/routes/Dashboard/Learn/Tour.tsx b/src/routes/Dashboard/Learn/Tour.tsx index d0c24f8dbe..540e279477 100644 --- a/src/routes/Dashboard/Learn/Tour.tsx +++ b/src/routes/Dashboard/Learn/Tour.tsx @@ -176,7 +176,7 @@ export function TourPage() { try { const file = await storage.rootFolder.addFile(newName, yamlContent); await navigate({ - to: APP_ROUTES.EDITOR_V2_PIPELINE, + to: APP_ROUTES.PIPELINE_EDITOR, params: { pipelineName: newName }, search: { fileId: file.id }, }); diff --git a/src/routes/Editor/Editor.tsx b/src/routes/Editor/Editor.tsx deleted file mode 100644 index 8be6246148..0000000000 --- a/src/routes/Editor/Editor.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import "@/styles/editor.css"; - -import { DndContext } from "@dnd-kit/core"; -import { ReactFlowProvider } from "@xyflow/react"; -import { useEffect } from "react"; - -import PipelineEditor from "@/components/Editor/PipelineEditor"; -import { InfoBox } from "@/components/shared/InfoBox"; -import { LoadingScreen } from "@/components/shared/LoadingScreen"; -import { BlockStack } from "@/components/ui/layout"; -import { Paragraph } from "@/components/ui/typography"; -import { useLoadComponentSpecFromPath } from "@/hooks/useLoadComponentSpecFromPath"; -import { addRecentlyViewed } from "@/hooks/useRecentlyViewed"; - -const Editor = () => { - const { componentSpec, error } = useLoadComponentSpecFromPath(); - useEffect(() => { - if (!componentSpec?.name) return; - addRecentlyViewed({ - type: "pipeline", - id: componentSpec.name, - name: componentSpec.name, - }); - }, [componentSpec?.name]); - - if (error) { - return ( - - - {error} - - - Tip: Pipelines are stored locally and are not shareable by URL. To - share a pipeline it needs to be exported. - - - ); - } - - if (!componentSpec) { - return ; - } - - return ( - - - - - - ); -}; - -export default Editor; diff --git a/src/routes/Editor/index.ts b/src/routes/Editor/index.ts deleted file mode 100644 index 100d029231..0000000000 --- a/src/routes/Editor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Editor"; diff --git a/src/routes/Import/Import.test.tsx b/src/routes/Import/Import.test.tsx index a2be0a0b2a..819b44f68b 100644 --- a/src/routes/Import/Import.test.tsx +++ b/src/routes/Import/Import.test.tsx @@ -142,7 +142,7 @@ describe("ImportPage", () => { await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith({ - to: "/editor-v2/Test%20Pipeline", + to: "/editor/Test%20Pipeline", }); }); }); diff --git a/src/routes/PipelineRun/PipelineRun.test.tsx b/src/routes/PipelineRun/PipelineRun.test.tsx deleted file mode 100644 index 14cb26f4f0..0000000000 --- a/src/routes/PipelineRun/PipelineRun.test.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { cleanup, render } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; - -import type { - ComponentSpecOutput, - GetExecutionInfoResponse, - GetGraphExecutionStateResponse, -} from "@/api/types.gen"; -import { ComponentSpecProvider } from "@/providers/ComponentSpecProvider"; - -import PipelineRun from "./PipelineRun"; - -// Mock the router and other dependencies -vi.mock("@tanstack/react-router", async (importOriginal) => { - return { - ...(await importOriginal()), - useNavigate: () => vi.fn(), - useParams: () => ({ id: "test-run-id-123" }), - useLocation: () => ({ - pathname: "/runs/test-run-id-123", - search: {}, - hash: "", - href: "/runs/test-run-id-123", - state: {}, - }), - useMatch: () => ({ - params: { id: "test-run-id-123" }, - }), - }; -}); - -vi.mock("@/routes/router", () => ({ - RUNS_BASE_PATH: "/runs", -})); - -vi.mock("@/providers/BackendProvider", () => ({ - useBackend: () => ({ - backendUrl: "http://localhost:8000", - configured: true, - available: true, - ready: true, - isConfiguredFromEnv: false, - isConfiguredFromRelativePath: false, - setEnvConfig: vi.fn(), - setRelativePathConfig: vi.fn(), - setBackendUrl: vi.fn(), - ping: vi.fn(), - }), -})); - -vi.mock("@/services/executionService", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - useFetchExecutionInfo: vi.fn(), - useFetchPipelineRun: () => ({ - data: null, - isLoading: false, - error: null, - isFetching: false, - refetch: () => {}, - enabled: false, - }), - }; -}); - -const mockUsePipelineRunData = vi.fn(); -vi.mock("@/hooks/usePipelineRunData", () => ({ - usePipelineRunData: () => mockUsePipelineRunData(), -})); - -const mockUseComponentSpec = vi.fn(); -vi.mock("@/providers/ComponentSpecProvider", async (importOriginal) => { - const actual = (await importOriginal()) as object; - return { - ...actual, - useComponentSpec: () => mockUseComponentSpec(), - }; -}); - -vi.mock("@/hooks/useDocumentTitle", () => ({ - useDocumentTitle: () => {}, -})); - -vi.mock("@/hooks/useFavicon", () => ({ - useFavicon: () => {}, -})); - -describe("", () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, - }); - - const mockComponentSpec: ComponentSpecOutput = { - name: "Test Pipeline", - description: "Test pipeline description", - inputs: [], - outputs: [], - metadata: { - annotations: undefined, - }, - implementation: { - graph: { - tasks: { - "task-1": { - componentRef: { - spec: { - name: "Task 1", - inputs: [], - outputs: [], - metadata: { - annotations: undefined, - }, - implementation: { - container: { - image: "test-image", - command: ["test-command"], - }, - }, - }, - }, - }, - "task-2": { - componentRef: { - spec: { - name: "Task 2", - inputs: [], - outputs: [], - metadata: { - annotations: undefined, - }, - implementation: { - container: { - image: "test-image-2", - command: ["test-command-2"], - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const mockExecutionDetails: GetExecutionInfoResponse = { - id: "test-execution-id", - pipeline_run_id: "test-run-id-123", - task_spec: { - componentRef: { - spec: mockComponentSpec, - }, - }, - child_task_execution_ids: { - "task-1": "execution-1", - "task-2": "execution-2", - }, - }; - - const mockExecutionState: GetGraphExecutionStateResponse = { - child_execution_status_stats: { - "execution-1": { SUCCEEDED: 1 }, - "execution-2": { RUNNING: 1 }, - }, - }; - - beforeEach(() => { - vi.clearAllMocks(); - - mockUsePipelineRunData.mockReturnValue({ - executionData: { - details: mockExecutionDetails, - state: mockExecutionState, - }, - rootExecutionId: "test-execution-id", - isLoading: false, - error: null, - isFetching: false, - refetch: vi.fn(), - }); - }); - - afterEach(() => { - cleanup(); - queryClient.clear(); - }); - - test("should render loading state initially", async () => { - // arrange - mockUseComponentSpec.mockReturnValue({ - componentSpec: null, - setComponentSpec: vi.fn(), - clearComponentSpec: vi.fn(), - currentSubgraphPath: ["root"], - navigateToSubgraph: vi.fn(), - navigateBack: vi.fn(), - navigateToPath: vi.fn(), - canNavigateBack: false, - graphSpec: {} as never, - isLoading: false, - isValid: true, - errors: [], - refetch: vi.fn(), - updateGraphSpec: vi.fn(), - saveComponentSpec: vi.fn(), - undoRedo: {} as never, - }); - - mockUsePipelineRunData.mockReturnValue({ - executionData: { - details: mockExecutionDetails, - state: mockExecutionState, - }, - rootExecutionId: "test-execution-id", - isLoading: true, - error: null, - isFetching: false, - refetch: vi.fn(), - }); - - // act - const { getByText } = render( - - - - - , - ); - - // assert - expect(getByText("Loading Pipeline Run")).toBeInTheDocument(); - }); -}); diff --git a/src/routes/PipelineRun/PipelineRun.tsx b/src/routes/PipelineRun/PipelineRun.tsx deleted file mode 100644 index bd30191e22..0000000000 --- a/src/routes/PipelineRun/PipelineRun.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { DndContext } from "@dnd-kit/core"; -import { useParams } from "@tanstack/react-router"; -import { ReactFlowProvider } from "@xyflow/react"; -import { useEffect } from "react"; - -import PipelineRunPage from "@/components/PipelineRun"; -import { InfoBox } from "@/components/shared/InfoBox"; -import { LoadingScreen } from "@/components/shared/LoadingScreen"; -import { NodesOverlayProvider } from "@/components/shared/ReactFlow/NodesOverlay/NodesOverlayProvider"; -import { RemoteAuthErrorView } from "@/components/shared/RemoteAuthErrorView"; -import { BlockStack } from "@/components/ui/layout"; -import { Paragraph } from "@/components/ui/typography"; -import { faviconManager } from "@/favicon"; -import { useDocumentTitle } from "@/hooks/useDocumentTitle"; -import { useTrackRecentlyViewedRun } from "@/hooks/useTrackRecentlyViewedRun"; -import { useBackend } from "@/providers/BackendProvider"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { - ExecutionDataProvider, - useExecutionData, -} from "@/providers/ExecutionDataProvider"; -import { getBackendStatusString } from "@/utils/backend"; -import type { ComponentSpec } from "@/utils/componentSpec"; -import { - flattenExecutionStatusStats, - getOverallExecutionStatusFromStats, -} from "@/utils/executionStatus"; -import { RemoteAuthError } from "@/utils/fetchWithErrorHandling"; - -const PipelineRunContent = () => { - const { setComponentSpec, clearComponentSpec, componentSpec } = - useComponentSpec(); - const { configured, available, ready } = useBackend(); - const params = useParams({ strict: false }); - const runId = - "id" in params && typeof params.id === "string" ? params.id : null; - - const { - details, - state, - isLoading: isLoadingCurrentLevelData, - error: currentLevelError, - rootDetails, - } = useExecutionData(); - - const isLoading = isLoadingCurrentLevelData; - const error = currentLevelError; - - useEffect(() => { - if (!details || !state) { - faviconManager.reset(); - return; - } - - const executionStatusStats = flattenExecutionStatusStats( - state.child_execution_status_stats, - ); - const overallStatus = - getOverallExecutionStatusFromStats(executionStatusStats); - const iconStatus = mapExecutionStatusToFavicon(overallStatus); - faviconManager.updateFavicon(iconStatus); - - return () => { - faviconManager.reset(); - }; - }, [details, state]); - - useEffect(() => { - if (rootDetails?.task_spec.componentRef.spec) { - setComponentSpec( - rootDetails.task_spec.componentRef.spec as ComponentSpec, - ); - } - - return () => { - clearComponentSpec(); - }; - }, [rootDetails, setComponentSpec, clearComponentSpec]); - - useDocumentTitle({ - "/runs/$id": (params) => - `Tangle - ${componentSpec?.name || ""} - ${params.id}`, - }); - - useTrackRecentlyViewedRun(runId, componentSpec?.name); - - if (isLoading || !ready) { - return ; - } - - if (!configured) { - return ( - - - Configure a backend to view this pipeline run. - - - ); - } - - if (!available) { - return ( - - - The configured backend is not available. - - - ); - } - - if (error && !rootDetails) { - if (error instanceof RemoteAuthError) { - return ; - } - const backendStatusString = getBackendStatusString(configured, available); - return ( - - - - {error.message} - - - {backendStatusString} - - - - ); - } - - return ; -}; - -const PipelineRun = () => { - const params = useParams({ strict: false }); - - if (!("id" in params) || typeof params.id !== "string") { - throw new Error("Missing required id parameter"); - } - - const id = params.id; - const subgraphExecutionId = - "subgraphExecutionId" in params && - typeof params.subgraphExecutionId === "string" - ? params.subgraphExecutionId - : undefined; - - return ( - - - - - - - - - - ); -}; - -export default PipelineRun; - -const mapExecutionStatusToFavicon = ( - status: string | undefined, -): "success" | "failed" | "loading" | "paused" | "default" => { - switch (status) { - case "SUCCEEDED": - return "success"; - case "FAILED": - case "SYSTEM_ERROR": - case "INVALID": - return "failed"; - case "RUNNING": - case "PENDING": - case "QUEUED": - case "WAITING_FOR_UPSTREAM": - case "CANCELLING": - case "UNINITIALIZED": - return "loading"; - case "CANCELLED": - case "SKIPPED": - return "paused"; - default: - return "default"; - } -}; diff --git a/src/routes/PipelineRun/index.ts b/src/routes/PipelineRun/index.ts deleted file mode 100644 index 17c7fd3f2d..0000000000 --- a/src/routes/PipelineRun/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./PipelineRun"; diff --git a/src/routes/appRoutes.ts b/src/routes/appRoutes.ts index e8397a0b1d..969d580b1b 100644 --- a/src/routes/appRoutes.ts +++ b/src/routes/appRoutes.ts @@ -1,4 +1,4 @@ -export const EDITOR_PATH = "/editor"; +const EDITOR_BASE_PATH = "/editor"; export const RUNS_BASE_PATH = "/runs"; const LEARN_BASE_PATH = "/learn"; const EDITOR_V2_BASE_PATH = "/editor-v2"; @@ -24,7 +24,8 @@ export const APP_ROUTES = { TOUR: TOUR_BASE_PATH, TOUR_DETAIL: `${TOUR_BASE_PATH}/$tourId`, IMPORT: IMPORT_PATH, - PIPELINE_EDITOR: `${EDITOR_PATH}/$name`, + EDITOR: EDITOR_BASE_PATH, + PIPELINE_EDITOR: `${EDITOR_BASE_PATH}/$pipelineName`, RUN_DETAIL: `${RUNS_BASE_PATH}/$id`, RUN_DETAIL_WITH_SUBGRAPH: `${RUNS_BASE_PATH}/$id/$subgraphExecutionId`, RUNS: RUNS_BASE_PATH, diff --git a/src/routes/editorRoutes.test.ts b/src/routes/editorRoutes.test.ts new file mode 100644 index 0000000000..5963d55a7b --- /dev/null +++ b/src/routes/editorRoutes.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest"; + +import { getDefaultEditorPath } from "./editorRoutes"; + +describe("editor routes", () => { + it("builds editor paths with encoded pipeline names", () => { + expect(getDefaultEditorPath("Pipeline 1")).toBe("/editor/Pipeline%201"); + }); +}); diff --git a/src/routes/editorRoutes.ts b/src/routes/editorRoutes.ts index 347a7c1c2b..a270e09e2f 100644 --- a/src/routes/editorRoutes.ts +++ b/src/routes/editorRoutes.ts @@ -1,17 +1,5 @@ -import { isFlagEnabled } from "@/components/shared/Settings/useFlags"; - -import { APP_ROUTES, EDITOR_PATH } from "./appRoutes"; - -function getLegacyEditorPath(pipelineName: string): string { - return `${EDITOR_PATH}/${encodeURIComponent(pipelineName)}`; -} - -function getEditorV2Path(pipelineName: string): string { - return `${APP_ROUTES.EDITOR_V2}/${encodeURIComponent(pipelineName)}`; -} +import { APP_ROUTES } from "./appRoutes"; export function getDefaultEditorPath(pipelineName: string): string { - return isFlagEnabled("v2_editor") - ? getEditorV2Path(pipelineName) - : getLegacyEditorPath(pipelineName); + return `${APP_ROUTES.EDITOR}/${encodeURIComponent(pipelineName)}`; } diff --git a/src/routes/router.ts b/src/routes/router.ts index e7c1655d7e..7abcb12cf9 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -33,10 +33,8 @@ import { LearnHomeView } from "./Dashboard/Learn/LearnHomeView"; import { LearnTipsView } from "./Dashboard/Learn/LearnTipsView"; import { LearnToursView } from "./Dashboard/Learn/LearnToursView"; import { TourPage } from "./Dashboard/Learn/Tour"; -import Editor from "./Editor"; import { ImportPage } from "./Import"; import NotFoundPage from "./NotFoundPage"; -import PipelineRun from "./PipelineRun"; import ArtifactPreviewPage from "./PipelineRun/ArtifactPreview"; import { AgentSettings } from "./Settings/sections/AgentSettings"; import { BackendSettings } from "./Settings/sections/BackendSettings"; @@ -51,7 +49,7 @@ import { RunViewV2 } from "./v2/pages/RunView/RunViewV2"; // Re-exported so existing `@/routes/router` import paths keep working after the // constants moved to the dependency-free `./appRoutes` leaf module. -export { APP_ROUTES, EDITOR_PATH, RUNS_BASE_PATH } from "./appRoutes"; +export { APP_ROUTES } from "./appRoutes"; declare module "@tanstack/react-router" { interface Register { @@ -99,7 +97,7 @@ const welcomeRoute = createRoute({ const dashboardRunsRoute = createRoute({ getParentRoute: () => dashboardRoute, - path: "/runs", + path: APP_ROUTES.DASHBOARD_RUNS, component: DashboardRunsView, }); @@ -258,12 +256,34 @@ const importRoute = createRoute({ }); const editorRoute = createRoute({ + getParentRoute: () => mainLayout, + path: APP_ROUTES.EDITOR, + component: EditorV2, +}); + +const editorPipelineRoute = createRoute({ getParentRoute: () => mainLayout, path: APP_ROUTES.PIPELINE_EDITOR, - component: Editor, - beforeLoad: ({ search }: { search: { name?: string } }) => { - const name = search.name || ""; - return { name }; + component: EditorV2, +}); + +const editorV2RedirectRoute = createRoute({ + getParentRoute: () => mainLayout, + path: APP_ROUTES.EDITOR_V2, + beforeLoad: ({ search }) => { + throw redirect({ to: APP_ROUTES.EDITOR, search }); + }, +}); + +const editorV2PipelineRedirectRoute = createRoute({ + getParentRoute: () => mainLayout, + path: APP_ROUTES.EDITOR_V2_PIPELINE, + beforeLoad: ({ params, search }) => { + throw redirect({ + to: APP_ROUTES.PIPELINE_EDITOR, + params: { pipelineName: params.pipelineName }, + search, + }); }, }); @@ -279,16 +299,16 @@ const huggingFaceAuthCallbackRoute = createRoute({ component: HuggingFaceAuthorizationResultScreen, }); -const runDetailRoute = createRoute({ +const runRoute = createRoute({ getParentRoute: () => mainLayout, path: APP_ROUTES.RUN_DETAIL, - component: PipelineRun, + component: RunViewV2, }); -const runDetailWithSubgraphRoute = createRoute({ +const runWithSubgraphRoute = createRoute({ getParentRoute: () => mainLayout, path: APP_ROUTES.RUN_DETAIL_WITH_SUBGRAPH, - component: PipelineRun, + component: RunViewV2, }); const secretsRouteTree = settingsSecretsRoute.addChildren([ @@ -306,61 +326,38 @@ const settingsRouteTree = settingsLayoutRoute.addChildren([ secretsRouteTree, ]); -const editorV2Route = createRoute({ +const runsV2RedirectRoute = createRoute({ getParentRoute: () => mainLayout, - path: APP_ROUTES.EDITOR_V2, - component: EditorV2, - beforeLoad: () => { - if (!isFlagEnabled("v2_editor")) { - throw redirect({ to: APP_ROUTES.DASHBOARD_PIPELINES }); - } - }, -}); - -const editorV2PipelineRoute = createRoute({ - getParentRoute: () => mainLayout, - path: APP_ROUTES.EDITOR_V2_PIPELINE, - component: EditorV2, - beforeLoad: ({ params }) => { - if (!isFlagEnabled("v2_editor")) { - throw redirect({ - to: APP_ROUTES.PIPELINE_EDITOR, - params: { name: params.pipelineName }, - }); - } + path: APP_ROUTES.RUNS_V2, + beforeLoad: ({ search }) => { + throw redirect({ to: APP_ROUTES.RUNS, search }); }, }); -const runV2Route = createRoute({ +const runV2RedirectRoute = createRoute({ getParentRoute: () => mainLayout, path: APP_ROUTES.RUN_DETAIL_V2, - component: RunViewV2, - beforeLoad: ({ params }) => { - if (!isFlagEnabled("v2_editor")) { - throw redirect({ - to: APP_ROUTES.RUN_DETAIL, - params: { id: params.id }, - search: (previous) => previous, - }); - } + beforeLoad: ({ params, search }) => { + throw redirect({ + to: APP_ROUTES.RUN_DETAIL, + params: { id: params.id }, + search, + }); }, }); -const runV2WithSubgraphRoute = createRoute({ +const runV2WithSubgraphRedirectRoute = createRoute({ getParentRoute: () => mainLayout, path: APP_ROUTES.RUN_DETAIL_V2_WITH_SUBGRAPH, - component: RunViewV2, - beforeLoad: ({ params }) => { - if (!isFlagEnabled("v2_editor")) { - throw redirect({ - to: APP_ROUTES.RUN_DETAIL_WITH_SUBGRAPH, - params: { - id: params.id, - subgraphExecutionId: params.subgraphExecutionId, - }, - search: (previous) => previous, - }); - } + beforeLoad: ({ params, search }) => { + throw redirect({ + to: APP_ROUTES.RUN_DETAIL_WITH_SUBGRAPH, + params: { + id: params.id, + subgraphExecutionId: params.subgraphExecutionId, + }, + search, + }); }, }); @@ -409,12 +406,14 @@ const appRouteTree = mainLayout.addChildren([ settingsRouteTree, importRoute, editorRoute, - runDetailRoute, - runDetailWithSubgraphRoute, - editorV2Route, - editorV2PipelineRoute, - runV2Route, - runV2WithSubgraphRoute, + editorPipelineRoute, + editorV2RedirectRoute, + editorV2PipelineRedirectRoute, + runRoute, + runWithSubgraphRoute, + runsV2RedirectRoute, + runV2RedirectRoute, + runV2WithSubgraphRedirectRoute, compareRoute, pipelineFoldersRoute, artifactPreviewRoute, diff --git a/src/routes/runRoutes.test.ts b/src/routes/runRoutes.test.ts index e8249cfdec..3b57d30c65 100644 --- a/src/routes/runRoutes.test.ts +++ b/src/routes/runRoutes.test.ts @@ -1,34 +1,16 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { isFlagEnabled } from "@/components/shared/Settings/useFlags"; +import { describe, expect, it } from "vitest"; import { getDefaultRunPath, getRunPath } from "./runRoutes"; -vi.mock("@/components/shared/Settings/useFlags", () => ({ - isFlagEnabled: vi.fn(), -})); - describe("run routes", () => { - beforeEach(() => { - vi.mocked(isFlagEnabled).mockReset(); - }); - - it("builds V1 and V2 paths, including subgraph executions", () => { - expect(getRunPath("run 1", "v1")).toBe("/runs/run%201"); - expect(getRunPath("run 1", "v2", "subgraph 1")).toBe( - "/runs-v2/run%201/subgraph%201", + it("builds run paths, including subgraph executions", () => { + expect(getRunPath("run 1")).toBe("/runs/run%201"); + expect(getRunPath("run 1", "subgraph 1")).toBe( + "/runs/run%201/subgraph%201", ); }); - it("uses the V2 run view when the V2 experience flag is enabled", () => { - vi.mocked(isFlagEnabled).mockReturnValue(true); - - expect(getDefaultRunPath("run-1")).toBe("/runs-v2/run-1"); - }); - - it("uses the V1 run view when the V2 experience flag is disabled", () => { - vi.mocked(isFlagEnabled).mockReturnValue(false); - + it("uses the canonical run route by default", () => { expect(getDefaultRunPath("run-1")).toBe("/runs/run-1"); }); }); diff --git a/src/routes/runRoutes.ts b/src/routes/runRoutes.ts index fe221d87a8..b7cffcf772 100644 --- a/src/routes/runRoutes.ts +++ b/src/routes/runRoutes.ts @@ -1,21 +1,15 @@ -import { isFlagEnabled } from "@/components/shared/Settings/useFlags"; - import { APP_ROUTES } from "./appRoutes"; -export type RunViewVersion = "v1" | "v2"; - export function getRunPath( runId: string | number, - version: RunViewVersion, subgraphExecutionId?: string, ): string { - const basePath = version === "v2" ? APP_ROUTES.RUNS_V2 : APP_ROUTES.RUNS; - const runPath = `${basePath}/${encodeURIComponent(runId)}`; + const runPath = `${APP_ROUTES.RUNS}/${encodeURIComponent(runId)}`; return subgraphExecutionId ? `${runPath}/${encodeURIComponent(subgraphExecutionId)}` : runPath; } export function getDefaultRunPath(runId: string | number): string { - return getRunPath(runId, isFlagEnabled("v2_editor") ? "v2" : "v1"); + return getRunPath(runId); } diff --git a/src/routes/v2/pages/Editor/EditorV2.tsx b/src/routes/v2/pages/Editor/EditorV2.tsx index af7ee78d24..ad115cb632 100644 --- a/src/routes/v2/pages/Editor/EditorV2.tsx +++ b/src/routes/v2/pages/Editor/EditorV2.tsx @@ -12,7 +12,6 @@ import { useFlagValue } from "@/components/shared/Settings/useFlags"; import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper"; import { InlineStack } from "@/components/ui/layout"; import { ComponentLibraryProvider } from "@/providers/ComponentLibraryProvider"; -import { ForcedSearchProvider } from "@/providers/ComponentLibraryProvider/ForcedSearchProvider"; import { DialogProvider } from "@/providers/DialogProvider/DialogProvider"; import { useTourMode } from "@/providers/TourProvider/TourModeContext"; import { TourSaveExploreDialog } from "@/providers/TourProvider/TourSaveExploreDialog"; @@ -171,15 +170,13 @@ function EditorV2Content({ pipelineRef }: { pipelineRef: PipelineRef | null }) { - {body} + {body} ); } -// Non-editor-v2 routes (e.g. `/tour/$tourId`) pass `pipelineRef` directly. -// Without a prop, we fall back to reading the route's params/search. export function EditorV2({ pipelineRef: pipelineRefProp, }: { diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx index c40df05614..8f4c523b0a 100644 --- a/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx +++ b/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx @@ -2,11 +2,13 @@ import { useState } from "react"; import { ComponentSearchEmptyStateSuggestions } from "@/components/shared/ComponentSearchEmptyStateSuggestions"; import { + ComponentItemFromUrl, ComponentMarkup, IONodeSidebarItem, StickyNoteSidebarItem, } from "@/components/shared/ReactFlow/FlowSidebar/components/ComponentItem"; import FolderItem from "@/components/shared/ReactFlow/FlowSidebar/components/FolderItem"; +import { useFlagValue } from "@/components/shared/Settings/useFlags"; import { Button } from "@/components/ui/button"; import { Icon } from "@/components/ui/icon"; import { BlockStack, InlineStack } from "@/components/ui/layout"; @@ -21,6 +23,8 @@ import type { ComponentSearchV2Result } from "./componentSearchV2Logic"; const INITIAL_VISIBLE_RESULT_COUNT = 10; const RESULT_PAGE_SIZE = 10; +const INPUT_AGGREGATOR_URL = + "https://raw.githubusercontent.com/TangleML/tangle-ui/refs/heads/master/public/assets/components/input_aggregator.component.yaml"; interface ComponentSearchResultsProps { query: string; @@ -88,6 +92,7 @@ export function ComponentSearchResults({ onClearRerank, onSuggestedSearch, }: ComponentSearchResultsProps) { + const inputAggregatorEnabled = useFlagValue("input-aggregator"); const isEmptyQuery = query.trim().length === 0; const [visibleResultCount, setVisibleResultCount] = useState( INITIAL_VISIBLE_RESULT_COUNT, @@ -119,6 +124,17 @@ export function ComponentSearchResults({ components: [ , , + ...(inputAggregatorEnabled + ? [ + , + ] + : []), ], }} icon="Cable" diff --git a/src/routes/v2/pages/Editor/components/EditorMenuBar/components/FileMenu.tsx b/src/routes/v2/pages/Editor/components/EditorMenuBar/components/FileMenu.tsx index be5ca5e362..8860fd8a0c 100644 --- a/src/routes/v2/pages/Editor/components/EditorMenuBar/components/FileMenu.tsx +++ b/src/routes/v2/pages/Editor/components/EditorMenuBar/components/FileMenu.tsx @@ -230,7 +230,7 @@ export function FileMenu() { } onImportComplete={(pipeline) => { navigate({ - to: APP_ROUTES.EDITOR_V2_PIPELINE, + to: APP_ROUTES.PIPELINE_EDITOR, params: { pipelineName: pipeline.name }, search: { fileId: pipeline.fileId }, }); diff --git a/src/routes/v2/pages/Editor/components/EditorMenuBar/components/useFileMenuState.ts b/src/routes/v2/pages/Editor/components/EditorMenuBar/components/useFileMenuState.ts index 51e7098dc1..81546365e7 100644 --- a/src/routes/v2/pages/Editor/components/EditorMenuBar/components/useFileMenuState.ts +++ b/src/routes/v2/pages/Editor/components/EditorMenuBar/components/useFileMenuState.ts @@ -78,7 +78,7 @@ export function useFileMenuState(): FileMenuState { const handleNewPipeline = async () => { const file = await createNewPipeline(storage); navigate({ - to: APP_ROUTES.EDITOR_V2_PIPELINE, + to: APP_ROUTES.PIPELINE_EDITOR, params: { pipelineName: file.storageKey }, search: { fileId: file.id }, }); @@ -86,7 +86,7 @@ export function useFileMenuState(): FileMenuState { const handlePipelineClick = (pipeline: PipelineRef) => { navigate({ - to: APP_ROUTES.EDITOR_V2_PIPELINE, + to: APP_ROUTES.PIPELINE_EDITOR, params: { pipelineName: pipeline.name }, search: { fileId: pipeline.fileId }, }); @@ -97,7 +97,7 @@ export function useFileMenuState(): FileMenuState { const file = await savePipelineAs(navigation, name, storage); notify(`Pipeline saved as "${name}"`, "success"); navigate({ - to: APP_ROUTES.EDITOR_V2_PIPELINE, + to: APP_ROUTES.PIPELINE_EDITOR, params: { pipelineName: name }, search: { fileId: file?.id }, }); diff --git a/src/routes/v2/pages/Editor/components/EmptyEditorState.tsx b/src/routes/v2/pages/Editor/components/EmptyEditorState.tsx index 9598254f5e..fbe6f38f31 100644 --- a/src/routes/v2/pages/Editor/components/EmptyEditorState.tsx +++ b/src/routes/v2/pages/Editor/components/EmptyEditorState.tsx @@ -21,7 +21,7 @@ export function EmptyEditorState() { const handlePipelineClick = (pipeline: PipelineRef) => { navigate({ - to: APP_ROUTES.EDITOR_V2_PIPELINE, + to: APP_ROUTES.PIPELINE_EDITOR, params: { pipelineName: pipeline.name }, search: { fileId: pipeline.fileId }, }); diff --git a/src/routes/v2/pages/Editor/hooks/usePipelineRename.ts b/src/routes/v2/pages/Editor/hooks/usePipelineRename.ts index d173f632ad..0008d159b5 100644 --- a/src/routes/v2/pages/Editor/hooks/usePipelineRename.ts +++ b/src/routes/v2/pages/Editor/hooks/usePipelineRename.ts @@ -18,7 +18,7 @@ export function usePipelineRename() { renamePipeline(spec, newName); await autoSave.save(); await navigate({ - to: APP_ROUTES.EDITOR_V2_PIPELINE, + to: APP_ROUTES.PIPELINE_EDITOR, params: { pipelineName: newName }, search: { fileId: pipelineFileStore.activePipelineFile?.id }, }); diff --git a/src/routes/v2/pages/Editor/store/actions/idGenerator.ts b/src/routes/v2/pages/Editor/store/actions/idGenerator.ts new file mode 100644 index 0000000000..220767a26d --- /dev/null +++ b/src/routes/v2/pages/Editor/store/actions/idGenerator.ts @@ -0,0 +1,3 @@ +import { IncrementingIdGenerator } from "@/models/componentSpec"; + +export const idGen = new IncrementingIdGenerator(); diff --git a/src/routes/v2/pages/Editor/store/actions/io.actions.ts b/src/routes/v2/pages/Editor/store/actions/io.actions.ts index 068bf6c2e7..9b0e964aeb 100644 --- a/src/routes/v2/pages/Editor/store/actions/io.actions.ts +++ b/src/routes/v2/pages/Editor/store/actions/io.actions.ts @@ -20,13 +20,13 @@ import { toConditionLiteral, } from "@/utils/conditionalExecution"; +import { idGen } from "./idGenerator"; import { propagateInputDelete, propagateInputRename, propagateOutputDelete, propagateOutputRename, } from "./io.parentPropagation"; -import { idGen } from "./utils"; export function addInput( undo: UndoGroupable, diff --git a/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts b/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts index d4710e671c..6482698084 100644 --- a/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts +++ b/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts @@ -14,7 +14,7 @@ import { PIPELINE_TAGS_ANNOTATION, } from "@/utils/annotations"; -import { idGen } from "./utils"; +import { idGen } from "./idGenerator"; export function renamePipeline( undo: UndoGroupable, diff --git a/src/routes/v2/pages/Editor/store/actions/task.actions.ts b/src/routes/v2/pages/Editor/store/actions/task.actions.ts index a8864509b2..ae6b58b585 100644 --- a/src/routes/v2/pages/Editor/store/actions/task.actions.ts +++ b/src/routes/v2/pages/Editor/store/actions/task.actions.ts @@ -19,8 +19,8 @@ import { TASK_COLOR_ANNOTATION, } from "@/utils/annotations"; +import { idGen } from "./idGenerator"; import { computeDiffComponentSpecs } from "./task.utils"; -import { idGen } from "./utils"; export function addTask( undo: UndoGroupable, diff --git a/src/routes/v2/pages/Editor/store/actions/utils.ts b/src/routes/v2/pages/Editor/store/actions/utils.ts index b43a4af24a..51cb4ec85c 100644 --- a/src/routes/v2/pages/Editor/store/actions/utils.ts +++ b/src/routes/v2/pages/Editor/store/actions/utils.ts @@ -1,10 +1,7 @@ import type { ComponentSpec } from "@/models/componentSpec"; -import { IncrementingIdGenerator } from "@/models/componentSpec"; import { editorRegistry } from "@/routes/v2/pages/Editor/nodes"; import type { NodeEntityType } from "@/routes/v2/shared/store/editorStore"; -export const idGen = new IncrementingIdGenerator(); - export function getNodeTypeFromId( spec: ComponentSpec | null, nodeId: string, diff --git a/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.test.tsx b/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.test.tsx index 5231c5dced..04573de6cc 100644 --- a/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.test.tsx +++ b/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.test.tsx @@ -119,7 +119,7 @@ describe("useRunViewSubgraphUrlSync", () => { expect(routerMocks.navigate).toHaveBeenCalledTimes(1); expect(routerMocks.navigate).toHaveBeenCalledWith({ - to: "/runs-v2/run-1/exec-sub", + to: "/runs/run-1/exec-sub", }); }); @@ -143,7 +143,7 @@ describe("useRunViewSubgraphUrlSync", () => { act(() => navigation.navigateToLevel(0)); expect(routerMocks.navigate).toHaveBeenCalledTimes(1); - expect(routerMocks.navigate).toHaveBeenCalledWith({ to: "/runs-v2/run-1" }); + expect(routerMocks.navigate).toHaveBeenCalledWith({ to: "/runs/run-1" }); }); it("resolves the target execution id from breadcrumb segments when going shallower", () => { @@ -162,7 +162,7 @@ describe("useRunViewSubgraphUrlSync", () => { expect(routerMocks.navigate).toHaveBeenCalledTimes(1); expect(routerMocks.navigate).toHaveBeenCalledWith({ - to: "/runs-v2/run-1/exec-a", + to: "/runs/run-1/exec-a", }); }); diff --git a/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.ts b/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.ts index 0f15b96ea4..f15394f0e3 100644 --- a/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.ts +++ b/src/routes/v2/pages/RunView/hooks/useRunViewSubgraphUrlSync.ts @@ -83,7 +83,7 @@ export function useRunViewSubgraphUrlSync() { if (!executionId) return; } - const target = getRunPath(runId, "v2", executionId); + const target = getRunPath(runId, executionId); if (window.location.pathname === target) return; lastPushedExecutionId.current = executionId; diff --git a/src/routes/v2/pages/RunView/nodes/TaskNode/context/RunViewTaskActions.tsx b/src/routes/v2/pages/RunView/nodes/TaskNode/context/RunViewTaskActions.tsx index a00ee0caf3..182b36a4ed 100644 --- a/src/routes/v2/pages/RunView/nodes/TaskNode/context/RunViewTaskActions.tsx +++ b/src/routes/v2/pages/RunView/nodes/TaskNode/context/RunViewTaskActions.tsx @@ -1,7 +1,7 @@ import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper"; import TaskActions from "@/components/shared/TaskDetails/Actions"; import { useHydrateComponentReference } from "@/hooks/useHydrateComponentReference"; -import type { TaskNodeContextType } from "@/providers/TaskNodeProvider"; +import type { TaskNodeContextType } from "@/types/taskNode"; import type { ComponentReference } from "@/utils/componentSpec"; interface RunViewTaskActionsProps { diff --git a/src/routes/v2/shared/components/AppMenuActions.tsx b/src/routes/v2/shared/components/AppMenuActions.tsx index 20de56208f..7b8bcb3762 100644 --- a/src/routes/v2/shared/components/AppMenuActions.tsx +++ b/src/routes/v2/shared/components/AppMenuActions.tsx @@ -5,8 +5,6 @@ import { OnboardingNavPill } from "@/components/Onboarding/OnboardingNavPill"; import { isAuthorizationRequired } from "@/components/shared/Authentication/helpers"; import { TopBarAuthentication } from "@/components/shared/Authentication/TopBarAuthentication"; import TooltipButton from "@/components/shared/Buttons/TooltipButton"; -import { EditorVersionToggle } from "@/components/shared/EditorVersionToggle"; -import { RunVersionToggle } from "@/components/shared/RunVersionToggle"; import { Icon } from "@/components/ui/icon"; import { InlineStack } from "@/components/ui/layout"; import { Link } from "@/components/ui/link"; @@ -28,8 +26,6 @@ export function AppMenuActions() { > {!tourMode && } - - {tourMode ? ( s.location.pathname }); - const isRunView = pathname.startsWith(RUNS_V2_PATH_PREFIX); + const isRunView = pathname.startsWith(`${APP_ROUTES.RUNS}/`); const path = navigation.navigationPath.map((entry, i) => i === 0 ? "Root" : entry.displayName, diff --git a/src/routes/v2/shared/overlays/edgeSelectionHighlight/useEdgeSelectionHighlightOverlay.ts b/src/routes/v2/shared/overlays/edgeSelectionHighlight/useEdgeSelectionHighlightOverlay.ts index 98a92b4a4d..9843b66a46 100644 --- a/src/routes/v2/shared/overlays/edgeSelectionHighlight/useEdgeSelectionHighlightOverlay.ts +++ b/src/routes/v2/shared/overlays/edgeSelectionHighlight/useEdgeSelectionHighlightOverlay.ts @@ -1,7 +1,7 @@ import type { Edge, ReactFlowState } from "@xyflow/react"; import { useStore } from "@xyflow/react"; -import { EdgeColor } from "@/components/shared/ReactFlow/FlowCanvas/Edges/utils"; +import { EDGE_SELECTED_COLOR } from "@/components/shared/ReactFlow/FlowCanvas/Edges/utils"; import { useCanvasOverlay } from "@/routes/v2/shared/hooks/useCanvasOverlay"; import type { CanvasOverlayConfig, @@ -48,7 +48,7 @@ function buildConfig(connectedIds: ReadonlySet): CanvasOverlayConfig { connectedIds.has(edge.source) && connectedIds.has(edge.target) ? { ...HIGHLIGHT_EFFECT, - style: { stroke: EdgeColor.Selected }, + style: { stroke: EDGE_SELECTED_COLOR }, } : DIMMED_EFFECT, }; diff --git a/src/services/pipelineService.ts b/src/services/pipelineService.ts index f39ca44254..69542d4dcd 100644 --- a/src/services/pipelineService.ts +++ b/src/services/pipelineService.ts @@ -3,10 +3,7 @@ import yaml from "js-yaml"; import { getAppSettings } from "@/appSettings"; import type { PipelineLibrary } from "@/types/pipelineLibrary"; import { downloadDataWithCache, loadObjectFromYamlData } from "@/utils/cache"; -import { - type ComponentSpec, - isGraphImplementation, -} from "@/utils/componentSpec"; +import { isGraphImplementation } from "@/utils/componentSpec"; import { type ComponentFileEntry, deleteComponentFileFromList, @@ -35,31 +32,6 @@ export const deletePipeline = async (name: string, onDelete?: () => void) => { } }; -export const useSavePipeline = (componentSpec: ComponentSpec) => { - const savePipeline = async (name?: string) => { - if (!componentSpec) { - return; - } - - const componentSpecWithNewName = { - ...componentSpec, - name: name ?? componentSpec.name ?? "Untitled Pipeline", - }; - - const componentSpecAsYaml = componentSpecToYaml(componentSpecWithNewName); - - await writeComponentToFileListFromText( - USER_PIPELINES_LIST_NAME, - componentSpecWithNewName.name, - componentSpecAsYaml, - ); - }; - - return { - savePipeline, - }; -}; - export const loadPipelineByName = async (name: string) => { const decodedName = decodeURIComponent(name); const appSettings = getAppSettings(); @@ -291,7 +263,3 @@ export async function importPipelineFromFile( }; } } - -export function getPipelineFile(pipelineName: string) { - return getComponentFileFromList(USER_PIPELINES_LIST_NAME, pipelineName); -} diff --git a/src/types/arguments.ts b/src/types/arguments.ts deleted file mode 100644 index 8c5533d442..0000000000 --- a/src/types/arguments.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ArgumentType, InputSpec } from "@/utils/componentSpec"; - -/** - * Argument input to the Argument Editor. - */ - -export type ArgumentInput = { - key: string; - value: ArgumentType; - initialValue: ArgumentType; - inputSpec: InputSpec; - isRemoved?: boolean; -}; diff --git a/src/types/taskNode.ts b/src/types/taskNode.ts index b14c1bff9d..3dfcde7801 100644 --- a/src/types/taskNode.ts +++ b/src/types/taskNode.ts @@ -1,6 +1,9 @@ import type { ArgumentType, ComponentReference, + HydratedComponentReference, + InputSpec, + OutputSpec, TaskSpec, } from "@/utils/componentSpec"; @@ -17,13 +20,49 @@ export interface TaskNodeData extends Record { nodeCallbacks?: NodeCallbacks; } -export type NodeAndTaskId = { +type NodeAndTaskId = { taskId: string; nodeId: string; }; export type TaskType = "task" | "input" | "output"; +interface TaskNodeContextState { + selected: boolean; + highlighted: boolean; + readOnly: boolean; + disabled: boolean; + connectable: boolean; + status?: string; + isCustomComponent: boolean; + isCollapsed: boolean; + dimensions: TaskNodeDimensions; +} + +interface TaskNodeContextCallbacks { + setArguments: (args: Record) => void; + setAnnotations: (annotations: Annotations) => void; + setCacheStaleness: (cacheStaleness: string | undefined) => void; + setCollapsed: (collapsed: boolean) => void; + onDelete?: () => void; + onDuplicate?: () => void; + onUpgrade?: () => void; + onSelect?: () => void; +} + +export interface TaskNodeContextType { + componentRef?: HydratedComponentReference; + taskSpec?: TaskSpec; + taskId?: string; + nodeId: string; + inputs: InputSpec[]; + outputs: OutputSpec[]; + name: string; + displayName: string; + state: TaskNodeContextState; + callbacks: TaskNodeContextCallbacks; +} + /* Note: Optional callbacks will cause TypeScript to break when applying the callbacks to the Nodes. */ export interface TaskNodeCallbacks { setArguments: (args: Record) => void; @@ -57,4 +96,4 @@ export type NodeCallbacks = { [K in keyof TaskNodeCallbacks]: CallbackWithIds; }; -export type TaskNodeDimensions = { w: number; h: number | undefined }; +type TaskNodeDimensions = { w: number; h: number | undefined }; diff --git a/src/utils/URL.ts b/src/utils/URL.ts index 63c0a2a9b9..ec69f5561f 100644 --- a/src/utils/URL.ts +++ b/src/utils/URL.ts @@ -1,4 +1,4 @@ -import { RUNS_BASE_PATH } from "@/routes/router"; +import { RUNS_BASE_PATH } from "@/routes/appRoutes"; import { BASE_URL, IS_GITHUB_PAGES } from "@/utils/constants"; const convertGcsUrlToBrowserUrl = ( diff --git a/src/utils/annotations.ts b/src/utils/annotations.ts index ab427e1fd1..a780cf86ee 100644 --- a/src/utils/annotations.ts +++ b/src/utils/annotations.ts @@ -1,7 +1,7 @@ import type { XYPosition } from "@xyflow/react"; import { getNodeTypeZIndexDefault } from "@/components/shared/ReactFlow/FlowCanvas/utils/zIndex"; -import type { AnnotationConfig, Annotations } from "@/types/annotations"; +import type { Annotations } from "@/types/annotations"; import { EDGE_CONDUITS_ANNOTATION, @@ -13,7 +13,6 @@ import { PIPELINE_TAGS_ANNOTATION, SDK_ANNOTATION, TASK_COLOR_ANNOTATION, - TASK_DISPLAY_NAME_ANNOTATION, ZINDEX_ANNOTATION, } from "./annotationKeys"; import type { ComponentSpec } from "./componentSpec"; @@ -35,24 +34,6 @@ export const SYSTEM_ANNOTATIONS = [ EDGE_CONDUITS_ANNOTATION, ]; -export const DEFAULT_COMMON_ANNOTATIONS: AnnotationConfig[] = [ - { - annotation: EDITOR_POSITION_ANNOTATION, - label: "Node position", - type: "json", - }, - { - annotation: TASK_DISPLAY_NAME_ANNOTATION, - label: "Display Name", - type: "string", - max: DISPLAY_NAME_MAX_LENGTH, - }, -]; - -export const HIDDEN_ANNOTATIONS = new Set([ - EDITOR_COLLAPSED_ANNOTATION, -]); - /** * Gets the value of an annotation. * @param annotations - The annotations object @@ -84,24 +65,6 @@ export function getAnnotationValue( : defaultValue; } -/** - * Sets the value of an annotation. - * @param annotations - The annotations object - * @param key - The key of the annotation - * @param value - The value to set - * @returns - */ -export function setAnnotation( - annotations: Annotations | undefined, - key: string, - value: string | undefined, -) { - return { - ...(annotations ?? {}), - [key]: value, - }; -} - /** * Checks if an annotation exists. * @param annotations - The annotations object @@ -119,31 +82,6 @@ function hasAnnotation( return Object.prototype.hasOwnProperty.call(annotations, key); } -/** - * Sets an annotation on a ComponentSpec. - * @param componentSpec - The ComponentSpec object - * @param annotationKey - The key of the annotation - * @param annotationValue - The value of the annotation - * @returns componentSpec with updated annotation - */ -export const setComponentSpecAnnotation = ( - componentSpec: ComponentSpec, - annotationKey: string, - annotationValue: string | undefined, -) => { - return { - ...componentSpec, - metadata: { - ...componentSpec.metadata, - annotations: setAnnotation( - componentSpec.metadata?.annotations, - annotationKey, - annotationValue, - ), - }, - }; -}; - /** * Sets an annotation on a ComponentSpec. * @param annotations - The annotations object @@ -230,24 +168,6 @@ export function ensureAnnotations( }; } -/* - * Removes an annotation from the annotations object. - * @param annotations - The annotations object - * @param key - The key of the annotation to remove - * @returns Updated annotations object with the specified annotation removed - */ -export function removeAnnotation( - annotations: Annotations | undefined, - key: string, -): Annotations | undefined { - if (!annotations || !hasAnnotation(annotations, key)) { - return annotations; - } - - const { [key]: _, ...rest } = annotations; - return rest; -} - /** * Gets the z-index from annotations. * @param annotations - The annotations object diff --git a/src/utils/componentSpec.ts b/src/utils/componentSpec.ts index 8bfbce42be..a6a8138c27 100644 --- a/src/utils/componentSpec.ts +++ b/src/utils/componentSpec.ts @@ -151,25 +151,6 @@ export type HydratedComponentReference = Omit< text: string; }; -type NotMaterializedComponentReference = Omit< - ComponentReference, - "spec" | "text" -> & { - spec: never; - text: never; -}; - -export function isNotMaterializedComponentReference( - componentReference: UnknownComponentReference, -): componentReference is NotMaterializedComponentReference { - return Boolean( - componentReference && - typeof componentReference === "object" && - !componentReference.spec && - !componentReference.text, - ); -} - export type DiscoverableComponentReference = Omit< ComponentReference, "digest" diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c89110d9ef..63fa8879e8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -53,7 +53,6 @@ export const PIPELINE_RUNS_STORE_NAME = "pipeline_runs"; export const USER_COMPONENTS_LIST_NAME = "user_components"; export const TOP_NAV_HEIGHT = 56; // px -export const BOTTOM_FOOTER_HEIGHT = 0; // px export const DEFAULT_NODE_DIMENSIONS = { w: 300, h: undefined }; @@ -69,17 +68,8 @@ export enum ComponentSearchFilter { EXACTMATCH = "Exact Match", } -export const DEFAULT_FILTERS = [ComponentSearchFilter.NAME]; - export const AUTOSAVE_DEBOUNCE_TIME_MS = 300; -export const KEYBOARD_SHORTCUTS = { - UNDO: "z", - REDO: "y", - MAC_META: "⌘", - WINDOWS_META: "Ctrl", -} as const; - // Container exit codes export const EXIT_CODE_OOM = 137; // SIGKILL (128 + 9) - Out of Memory diff --git a/src/utils/searchUtils.ts b/src/utils/searchUtils.ts index 2eb5ec0033..dcadd84af3 100644 --- a/src/utils/searchUtils.ts +++ b/src/utils/searchUtils.ts @@ -77,34 +77,6 @@ function checkSearchFilterComponent( } } -export function checkArtifactMatchesSearchFilters( - searchTerm: string, - filters: string[], - artifact: InputSpec | OutputSpec, - exactMatch: boolean = false, -): boolean { - exactMatch = exactMatch || filters.includes(ComponentSearchFilter.EXACTMATCH); - return filters.some((filter) => { - if ( - filter === ComponentSearchFilter.INPUTNAME || - filter === ComponentSearchFilter.OUTPUTNAME - ) { - return checkNameMatchesSearch(artifact.name, searchTerm, exactMatch); - } - if ( - filter === ComponentSearchFilter.INPUTTYPE || - filter === ComponentSearchFilter.OUTPUTTYPE - ) { - return containsSearchTerm( - JSON.stringify(artifact.type), - searchTerm, - exactMatch, - ); - } - return false; - }); -} - /** * Checks if variations of a name * string matches the search term. diff --git a/src/utils/subgraphUtils.ts b/src/utils/subgraphUtils.ts index 53e97acc20..bb7a76d430 100644 --- a/src/utils/subgraphUtils.ts +++ b/src/utils/subgraphUtils.ts @@ -434,7 +434,3 @@ export const updateSubgraphSpec = ( }, }; }; - -export const isViewingSubgraph = (subgraphPath: string[]): boolean => { - return subgraphPath.length > 1; -}; diff --git a/src/utils/unique.ts b/src/utils/unique.ts index cef7c20b13..f2778d284c 100644 --- a/src/utils/unique.ts +++ b/src/utils/unique.ts @@ -46,35 +46,3 @@ export const getUniqueTaskName = ( export const getUniqueName = (names: string[], name: string = "Untitled") => { return makeNameUniqueByAddingIndex(name, new Set(names)); }; - -export const validateTaskName = ( - name: string, - graphSpec: GraphSpec, - checkId = false, -): string | null => { - const trimmedName = name.trim(); - - if (!trimmedName) { - return "Name cannot be empty"; - } - - if (checkId && new Set(Object.keys(graphSpec.tasks)).has(trimmedName)) { - return "A task with this id already exists"; - } - - if ( - new Set( - Object.values(graphSpec.tasks).map( - (task) => task.componentRef.spec?.name, - ), - ).has(trimmedName) - ) { - return "A task with this name already exists"; - } - - if (!/^[a-zA-Z0-9 _-]+$/.test(trimmedName)) { - return "Name cannot contain special characters"; - } - - return null; -}; diff --git a/src/utils/validations.ts b/src/utils/validations.ts index 0af5e9dba8..2b32db8eed 100644 --- a/src/utils/validations.ts +++ b/src/utils/validations.ts @@ -34,10 +34,6 @@ export interface ValidationError { outputName?: string; } -export function isFixableIssue(issue: ComponentValidationIssue): boolean { - return issue.type === "argument"; -} - export function validateArguments( inputs: InputSpec[], taskArguments: Record, diff --git a/tests/e2e/component-editor.spec.ts b/tests/e2e/component-editor.spec.ts index 41793c7b17..7c430cef1e 100644 --- a/tests/e2e/component-editor.spec.ts +++ b/tests/e2e/component-editor.spec.ts @@ -58,21 +58,14 @@ test.describe("Component Editor", () => { await page.getByRole("button", { name: "Save" }).click(); - await dropComponentFromLibraryOnCanvas( + const node = await dropComponentFromLibraryOnCanvas( page, "User Components", "Filter text", ); + await expect(node).toBeVisible(); - const usedOnCanvasFolder = await openComponentLibFolder( - page, - "Used in Pipeline", - ); - await expect(usedOnCanvasFolder.locator("li")).toHaveCount(1); - - // remove the component from the canvas await removeComponentFromCanvas(page, "Filter text"); - await expect(usedOnCanvasFolder.locator("li")).toHaveCount(0); const userComponentsFolder = await openComponentLibFolder( page, diff --git a/tests/e2e/flowcanvas.spec.ts b/tests/e2e/flowcanvas.spec.ts index 4ff113363b..5abbfb1928 100644 --- a/tests/e2e/flowcanvas.spec.ts +++ b/tests/e2e/flowcanvas.spec.ts @@ -9,7 +9,6 @@ import { locateFlowViewport, openComponentLibFolder, panCanvas, - waitForContextPanel, zoomIn, zoomOut, } from "./helpers"; @@ -53,12 +52,11 @@ test.describe("FlowCanvas Basic Functionality", () => { await expect(node).toBeVisible(); - await waitForContextPanel(page, "pipeline-details"); - await node.click(); - await expect(node).toHaveClass(/\bselected\b/); - await waitForContextPanel(page, "task-overview"); + await expect(node.locator('[data-tour-card="task"]')).toHaveClass( + /ring-edge-selected/, + ); }); test("should connect two nodes satisfying the required field requirements", async ({ @@ -101,20 +99,16 @@ test.describe("FlowCanvas Basic Functionality", () => { await expect(outputPin).toBeInViewport(); await expect(inputPin).toBeInViewport(); - await expect(inputPin).toHaveAttribute("data-invalid", "true"); + await expect(inputPin).toHaveClass(/bg-red-700/); await outputPin.hover(); await page.mouse.down(); await inputPin.hover(); await page.mouse.up(); - await expect(inputPin).toHaveAttribute("data-invalid", "false"); + await expect(inputPin).not.toHaveClass(/bg-red-700/); - const edgesContainer = page.locator(".react-flow__edges"); - const edge = edgesContainer.locator( - '[data-testid="rf__edge-Chicago Taxi Trips dataset_Table-Train XGBoost model on CSV_training_data"]', - ); - await expect(edge).toBeVisible(); + await expect(page.locator(".react-flow__edge")).toHaveCount(1); const inputHandle = nodeB.locator( '[data-testid="input-handle-training_data"]', @@ -122,7 +116,7 @@ test.describe("FlowCanvas Basic Functionality", () => { await expect(inputHandle).toBeVisible(); await expect( - nodeB.locator('[data-testid="input-handle-value-training_data"]'), - ).toHaveText(`→ Chicago Taxi Trips dataset.Table`); + nodeB.getByText("→ Chicago Taxi Trips dataset.Table"), + ).toBeVisible(); }); }); diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts index 05fb4df550..7198e3b31d 100644 --- a/tests/e2e/helpers.ts +++ b/tests/e2e/helpers.ts @@ -5,19 +5,6 @@ import { expect, type Locator, type Page } from "@playwright/test"; * Waits for the React Flow canvas to be fully loaded (past Suspense loading state). */ export async function createNewPipeline(page: Page): Promise { - // These shared E2E helpers cover the legacy editor until v2 has matching coverage. - await page.addInitScript(() => { - const flags = JSON.parse( - window.localStorage.getItem("betaFlags") ?? "{}", - ) as Record; - - window.localStorage.setItem( - "betaFlags", - JSON.stringify({ ...flags, v2_editor: false }), - ); - window.localStorage.setItem("seen-editor-v2-welcome", JSON.stringify(true)); - }); - await page.goto("/"); await page.getByTestId("new-pipeline-button").click(); await waitForFlowCanvas(page); @@ -193,7 +180,7 @@ export async function dragComponentToCanvas( * Locates a task node by its name */ function locateNodeByName(page: Page, nodeName: string): Locator { - return page.locator(`[data-testid="rf__node-task_${nodeName}"]`); + return page.locator(`[data-task-name="${nodeName}"]`); } /** @@ -203,16 +190,9 @@ function locateNodeByName(page: Page, nodeName: string): Locator { /** * Locates the context panel container */ -function locateContextPanelContainer(page: Page): Locator { - return page.locator('[data-testid="context-panel-container"]'); -} - -/** - * Locates a specific context panel by name - */ function locateContextPanel(page: Page, panelName: string): Locator { - const container = locateContextPanelContainer(page); - return container.locator(`[data-context-panel="${panelName}"]`); + const windowId = panelName === "task-overview" ? "context-panel" : panelName; + return page.locator(`[data-dock-window-content="${windowId}"]`); } /** @@ -262,31 +242,16 @@ export async function removeComponentFromCanvas( ): Promise { const node = locateNodeByName(page, componentName); await node.click(); - await node.press("Delete"); + await page.getByRole("button", { name: "Node" }).click(); + await page.getByRole("menuitem", { name: "Delete Task" }).click(); - // Wait for confirmation dialog to appear and be interactive - const confirmDialog = page.locator('[role="alertdialog"]'); - const continueButton = confirmDialog.getByText("Continue"); - await expect(continueButton).toBeVisible(); - await continueButton.click(); - - // Wait for the node to be removed from the DOM await expect(node).toBeHidden(); - await expect(confirmDialog).toBeHidden(); } /** * todo: helper to position node relatively to another node accounting for the Canvas transform */ -/** - * Asserts the current state of the search functionality - * @param page - Playwright page object - * @param options - Search state options to verify - * @param options.searchTerm - Expected search input value - * @param options.searchFilterCount - Expected filter count badge text (optional) - * @param options.searchResultsCount - Expected result count or "*" for any (optional) - */ /** * Settings Helpers */ @@ -327,6 +292,7 @@ export async function setBetaFlag( if (isChecked !== enabled) { await flagSwitch.click(); } + await expect(flagSwitch).toBeChecked({ checked: enabled }); } /** @@ -398,43 +364,3 @@ export async function removeSecret( `Secret "${secretName}" should be removed from list`, ).toBeHidden(); } - -export async function assertSearchState( - page: Page, - options: { - searchTerm: string; - searchFilterCount?: string; - searchResultsCount?: string; - } = { - searchTerm: "", - }, -) { - const { searchTerm, searchFilterCount, searchResultsCount } = options; - - // Assert search input value - const searchInput = page.getByTestId("search-input"); - await expect(searchInput).toHaveValue(searchTerm); - - // Assert search filter counter - const searchFilterCounter = page.getByTestId("search-filter-counter"); - if (searchFilterCount) { - await expect(searchFilterCounter).toHaveText(searchFilterCount); - } else { - await expect(searchFilterCounter).toBeHidden(); - } - - // Assert search results header and count - const searchResultsHeader = page.getByTestId("search-results-header"); - if (searchResultsCount && Number(searchResultsCount) > 0) { - await expect(searchResultsHeader).toHaveText( - `Search Results (${searchResultsCount})`, - ); - const componentItem = page.getByTestId("component-item"); - await expect(componentItem).toHaveCount(Number(searchResultsCount)); - } else if (searchResultsCount && searchResultsCount === "*") { - await expect(searchResultsHeader).toBeVisible(); - await expect(searchResultsHeader).toContainText("Search Results"); - } else { - await expect(searchResultsHeader).toBeHidden(); - } -} diff --git a/tests/e2e/published-componentlib.spec.ts b/tests/e2e/published-componentlib.spec.ts index 3bf12003eb..d0397f3a80 100644 --- a/tests/e2e/published-componentlib.spec.ts +++ b/tests/e2e/published-componentlib.spec.ts @@ -1,12 +1,8 @@ import { expect, type Page, test } from "@playwright/test"; import { - assertSearchState, createNewPipeline, dragComponentToCanvas, - dropComponentFromLibraryOnCanvas, - locateComponentInFolder, - locateFlowCanvas, locateFolderByName, openComponentLibFolder, removeComponentFromCanvas, @@ -97,42 +93,32 @@ test.describe("Published Component Library", () => { const searchResultsHeader = page.getByTestId("search-results-header"); await expect(searchResultsHeader).toBeVisible(); - await expect(searchResultsHeader).toHaveText("Search Results (5)"); + await expect(searchResultsHeader).toHaveText("Search Results (10 of 13)"); const componentItem = page.getByTestId("component-item"); - await expect(componentItem).toHaveCount(5); + await expect(componentItem).toHaveCount(10); await clearSearch(page); }); - test("components from search results can be added and removed from favorites folder", async () => { - // add component to the favorites by clicking the star icon + test("components from search results can be favorited and unfavorited", async () => { await searchForComponent(page, "GCS"); const downloadFromGCS = await findComponentFromSearchResults( page, "Download from GCS", ); + const favoriteButton = downloadFromGCS.getByTestId("favorite-star"); + const favoriteIcon = favoriteButton.locator("svg"); - await downloadFromGCS.getByTestId("favorite-star").click(); + await expect(favoriteIcon).toHaveAttribute("fill", "none"); + await favoriteButton.click(); + await expect(favoriteIcon).not.toHaveAttribute("fill", "none"); - await clearSearch(page); - - // expect the component to be in the favorites folder - const favoritesFolder = await openComponentLibFolder( - page, - "Favorite Components", - ); - await expect(favoritesFolder.locator("li")).toHaveCount(1); + await favoriteButton.click(); + await expect(favoriteIcon).toHaveAttribute("fill", "none"); - // unstar the component - const downloadFromGCSFavorite = locateComponentInFolder( - favoritesFolder, - "Download from GCS", - ); - await downloadFromGCSFavorite.getByTestId("favorite-star").click(); - - await expect(favoritesFolder.locator("li")).toHaveCount(0); + await clearSearch(page); }); test("component details can be opened as a dialog", async () => { @@ -159,7 +145,7 @@ test.describe("Published Component Library", () => { await clearSearch(page); }); - test("components from search results can be dragged to the canvas and appear in the used in pipeline folder", async () => { + test("components from search results can be dragged to the canvas", async () => { await searchForComponent(page, "GCS"); const downloadFromGCS = await findComponentFromSearchResults( @@ -170,114 +156,10 @@ test.describe("Published Component Library", () => { await clearSearch(page); - const usedOnCanvasFolder = await openComponentLibFolder( - page, - "Used in Pipeline", - ); - await expect(usedOnCanvasFolder.locator("li")).toHaveCount(1); + const node = page.locator('[data-task-name="Download from GCS"]'); + await expect(node).toBeVisible(); - // remove the component from the canvas await removeComponentFromCanvas(page, "Download from GCS"); - await expect(usedOnCanvasFolder.locator("li")).toHaveCount(0); - }); - - test("search results can be highlighted on input pin click", async () => { - await openComponentLibFolder(page, "Standard library"); - await openComponentLibFolder(page, "Data manipulation"); - await openComponentLibFolder(page, "CSV"); - - // drop component on the canvas - const node = await dropComponentFromLibraryOnCanvas( - page, - "CSV", - "Select columns using Pandas on CSV data", - ); - await expect( - node, - "Dropped component should be on the canvas before clicking its pin", - ).toBeVisible(); - - const inputHandle = page.getByTestId("input-handle-table"); - await expect(inputHandle).toBeVisible(); - await inputHandle.click(); - - const outputConnection = page.getByTestId( - "output-connection-transformed_table", - ); - const inputConnection = page.getByTestId("input-connection-table"); - await expect(outputConnection).toHaveAttribute("data-highlighted", "true"); - - // assert highlighting - await expect(outputConnection).toHaveAttribute("data-highlighted", "true"); - await expect(inputConnection).toHaveAttribute("data-highlighted", "false"); - await expect(inputConnection).toHaveAttribute("data-selected", "true"); - - // reset highlighting after clicking on the canvas - await locateFlowCanvas(page).click(); - - // resets selection after clicking on the canvas - await expect(inputConnection).toHaveAttribute("data-highlighted", "false"); - await expect(outputConnection).toHaveAttribute("data-highlighted", "false"); - - // search should be reset - await assertSearchState(page, { - searchTerm: "", - }); - - // remove the component from the canvas - await removeComponentFromCanvas( - page, - "Select columns using Pandas on CSV data", - ); - }); - - test("search results can be highlighted on output pin click", async () => { - await openComponentLibFolder(page, "Standard library"); - await openComponentLibFolder(page, "Data manipulation"); - await openComponentLibFolder(page, "CSV"); - - const node = await dropComponentFromLibraryOnCanvas( - page, - "CSV", - "Select columns using Pandas on CSV data", - ); - await expect( - node, - "Dropped component should be on the canvas before clicking its pin", - ).toBeVisible(); - - const outputHandle = page.getByTestId("output-handle-transformed_table"); - await expect(outputHandle).toBeVisible(); - await outputHandle.click(); - - const outputConnection = page.getByTestId( - "output-connection-transformed_table", - ); - const inputConnection = page.getByTestId("input-connection-table"); - await expect(outputConnection).toHaveAttribute("data-selected", "true"); - - // assert highlighting - await expect(outputConnection).toHaveAttribute("data-highlighted", "false"); - await expect(outputConnection).toHaveAttribute("data-selected", "true"); - await expect(inputConnection).toHaveAttribute("data-highlighted", "true"); - - // reset highlighting after clicking on the canvas - await locateFlowCanvas(page).click(); - - // resets selection after clicking on the canvas - await expect(inputConnection).toHaveAttribute("data-highlighted", "false"); - await expect(outputConnection).toHaveAttribute("data-selected", "false"); - - // search should be reset - await assertSearchState(page, { - searchTerm: "", - }); - - // remove the component from the canvas - await removeComponentFromCanvas( - page, - "Select columns using Pandas on CSV data", - ); }); }); diff --git a/tests/e2e/secrets-in-arguments.spec.ts b/tests/e2e/secrets-in-arguments.spec.ts index f9b0e04c65..e16fae0844 100644 --- a/tests/e2e/secrets-in-arguments.spec.ts +++ b/tests/e2e/secrets-in-arguments.spec.ts @@ -17,12 +17,16 @@ import { */ test.describe.configure({ mode: "serial" }); -function argumentInput(page: Page, argumentName: string): Locator { +function argumentRow(page: Page, argumentName: string): Locator { return page.locator( - `[data-context-panel="task-overview"] [data-testid="argument-input-field"][data-argument-name="${argumentName}"] input`, + `[data-dock-window-content="context-panel"] [data-argument-name="${argumentName}"]`, ); } +function argumentInput(page: Page, argumentName: string): Locator { + return argumentRow(page, argumentName).getByTestId("argument-input"); +} + /** * Fills a task argument and commits it before returning. * @@ -87,7 +91,7 @@ test.describe("Secrets in Component Arguments", () => { }); test("select component and verify Arguments tab is shown", async () => { - const node = page.locator(`[data-testid="rf__node-task_${componentName}"]`); + const node = page.locator(`[data-task-name="${componentName}"]`); await node.click(); const taskOverviewPanel = await waitForContextPanel(page, "task-overview"); @@ -103,25 +107,16 @@ test.describe("Secrets in Component Arguments", () => { }); test("assign secret to GITHUB_PAT argument", async () => { - const taskOverviewPanel = page.locator( - '[data-context-panel="task-overview"]', - ); - - const githubPatField = taskOverviewPanel.locator( - '[data-testid="argument-input-field"][data-argument-name="GITHUB_PAT"]', - ); + const githubPatField = argumentRow(page, "GITHUB_PAT"); await expect( githubPatField, "GITHUB_PAT argument field should be visible", ).toBeVisible(); await githubPatField.hover(); - - const directSecretButton = githubPatField.getByTestId( - "open-secret-dialog-button", - ); - await expect(directSecretButton).toBeVisible(); - await directSecretButton.click(); + await githubPatField.getByTestId("thunder-menu-trigger").click(); + await page.getByRole("menuitem", { name: "Dynamic Data" }).hover(); + await page.getByRole("menuitem", { name: "Select Secret..." }).click(); const selectSecretDialog = page.getByTestId("select-secret-dialog"); await expect( @@ -143,17 +138,10 @@ test.describe("Secrets in Component Arguments", () => { "Dialog should close after selection", ).toBeHidden(); - const secretInput = githubPatField.getByTestId( - "dynamic-data-argument-input", - ); await expect( - secretInput, - "Secret input should be visible after selection", - ).toBeVisible(); - await expect(secretInput).toHaveAttribute( - "data-secret-name", - testSecretName, - ); + githubPatField, + "Selected secret should appear in the argument field", + ).toContainText(testSecretName); }); test("fill other required arguments with dummy values", async () => { @@ -173,11 +161,13 @@ test.describe("Secrets in Component Arguments", () => { test("Submit Run button should be enabled with all arguments filled", async () => { await page.locator(".react-flow__pane").click(); - const submitButton = page.getByRole("button", { name: /Submit Run/i }); + const submitButton = page + .getByTestId("runs-and-submission-buttons") + .getByRole("button", { name: "Submit Run" }); await expect( submitButton, - "Submit Run button should be visible", - ).toBeVisible(); + "Submit Run button should be enabled", + ).toBeEnabled(); }); }); @@ -227,7 +217,7 @@ async function dropGithubFakeCommitPushComponent(page: Page) { await canvas.dispatchEvent("drop", { dataTransfer }); const componentName = "Github - Fake Commit Push"; - const node = page.locator(`[data-testid="rf__node-task_${componentName}"]`); + const node = page.locator(`[data-task-name="${componentName}"]`); await expect(node, "Component node should appear on canvas").toBeVisible({ timeout: 10000, }); diff --git a/tests/e2e/sticky-note.spec.ts b/tests/e2e/sticky-note.spec.ts index 4aaf11fc54..7f5fbacb44 100644 --- a/tests/e2e/sticky-note.spec.ts +++ b/tests/e2e/sticky-note.spec.ts @@ -120,7 +120,9 @@ test.describe("Sticky Note (FlexNode) Functionality", () => { const stickyNote = locateStickyNote(page); await stickyNote.click(); - const contextPanel = page.getByTestId("context-panel-container"); + const contextPanel = page.locator( + '[data-dock-window-content="context-panel"]', + ); await expect(contextPanel).toBeVisible(); await expect( @@ -252,8 +254,12 @@ test.describe("Sticky Note (FlexNode) Functionality", () => { const lastNote = allStickyNotes.last(); await lastNote.click(); - const contextPanel = page.getByTestId("context-panel-container"); - await expect(contextPanel.locator('text="Stacking"')).toBeVisible(); + const contextPanel = page.locator( + '[data-dock-window-content="context-panel"]', + ); + await expect( + contextPanel.getByText("Stacking", { exact: true }), + ).toBeVisible(); const stackingControls = page.getByTestId("stacking-controls"); await expect(stackingControls).toBeVisible(); @@ -310,7 +316,7 @@ test.describe("Sticky Note (FlexNode) Functionality", () => { "Selection toolbar should appear after box selection", ).toBeVisible({ timeout: 5000 }); - const duplicateButton = page.getByTestId("selection-duplicate-nodes"); + const duplicateButton = page.getByTestId("selection-duplicate"); await expect( duplicateButton, "Duplicate button should appear in selection toolbar", @@ -328,7 +334,8 @@ test.describe("Sticky Note (FlexNode) Functionality", () => { test("should serialize sticky notes in pipeline annotations (YAML export)", async () => { await fitToView(page); - const exportButton = page.getByTestId("action-Export Pipeline"); + await page.getByRole("button", { name: "File" }).click(); + const exportButton = page.getByRole("menuitem", { name: "Export" }); await expect(exportButton).toBeVisible(); const downloadPromise = page.waitForEvent("download"); diff --git a/tests/e2e/validation-ui.spec.ts b/tests/e2e/validation-ui.spec.ts index 7720fa2ce2..dbb103ca57 100644 --- a/tests/e2e/validation-ui.spec.ts +++ b/tests/e2e/validation-ui.spec.ts @@ -13,19 +13,19 @@ const CHICAGO_TAXI_COMPONENT = "Chicago Taxi Trips dataset"; const XGBOOST_COMPONENT = "Train XGBoost model on CSV"; function locateTaskNode(page: Page, taskName: string): Locator { - return page.locator(`[data-testid="rf__node-task_${taskName}"]`); + return page.locator(`[data-task-name="${taskName}"]`); } -async function assertValidationState( +async function expectValidationState( page: Page, - expectedTitle: string, + expectedText: string, ): Promise { await page.keyboard.press("Escape"); - await waitForContextPanel(page, "pipeline-details"); + const pipelineDetails = await waitForContextPanel(page, "pipeline-details"); await expect( - page.getByTestId("info-box-title"), - `Validation should show "${expectedTitle}"`, - ).toHaveText(expectedTitle); + pipelineDetails.getByText(expectedText, { exact: true }), + `Validation should show "${expectedText}"`, + ).toBeVisible(); } test.describe.configure({ mode: "serial" }); @@ -50,13 +50,10 @@ test.describe("Pipeline Validation UI", () => { CHICAGO_TAXI_COMPONENT, ); - await waitForContextPanel(page, "pipeline-details"); - - const validationInfoBox = page.getByTestId("info-box-success"); - await expect(validationInfoBox).toBeVisible(); - await expect(page.getByTestId("info-box-title")).toHaveText( - "No validation issues found", - ); + const pipelineDetails = await waitForContextPanel(page, "pipeline-details"); + await expect( + pipelineDetails.getByText("No validation issues", { exact: true }), + ).toBeVisible(); }); test("shows 2 errors when adding component with missing required inputs", async () => { @@ -79,38 +76,28 @@ test.describe("Pipeline Validation UI", () => { }, ); - await waitForContextPanel(page, "pipeline-details"); - - const validationInfoBox = page.getByTestId("info-box-error"); + const pipelineDetails = await waitForContextPanel(page, "pipeline-details"); await expect( - validationInfoBox, - "Validation info box should show errors", + pipelineDetails.getByText("2 errors", { exact: true }), ).toBeVisible(); - await expect(page.getByTestId("info-box-title")).toHaveText( - "2 errors detected", - ); }); test("shows expanded view of validation errors", async () => { - const validationGroup = page.getByTestId("validation-group"); - await expect(validationGroup).toBeVisible(); - - const groupTrigger = page.getByTestId("validation-group-trigger"); - await groupTrigger.click(); + const pipelineDetails = await waitForContextPanel(page, "pipeline-details"); + await pipelineDetails.getByRole("button", { name: "2 errors" }).click(); - const validationIssues = page.getByTestId("validation-issue"); + const validationIssues = pipelineDetails.getByRole("button", { + name: /^TASK /, + }); await expect(validationIssues).toHaveCount(2); - - await expect(validationIssues.first()).toHaveAttribute( - "data-issue-level", - "error", - ); }); test("clicking validation error navigates to task on canvas", async () => { const xgboostNode = locateTaskNode(page, XGBOOST_COMPONENT); - - const validationIssues = page.getByTestId("validation-issue"); + const pipelineDetails = await waitForContextPanel(page, "pipeline-details"); + const validationIssues = pipelineDetails.getByRole("button", { + name: /^TASK /, + }); await validationIssues.first().click(); await expect( @@ -138,12 +125,12 @@ test.describe("Pipeline Validation UI", () => { await inputPin.hover(); await page.mouse.up(); - const edge = page.locator( - `[data-testid="rf__edge-${CHICAGO_TAXI_COMPONENT}_Table-${XGBOOST_COMPONENT}_training_data"]`, - ); - await expect(edge, "Edge should be created after connection").toBeVisible(); + await expect( + page.locator(".react-flow__edge"), + "Edge should be created after connection", + ).toHaveCount(1); - await assertValidationState(page, "1 error detected"); + await expectValidationState(page, "1 error"); }); test("creating input node via CMD+drag changes validation to 1 warning", async () => { @@ -180,20 +167,24 @@ test.describe("Pipeline Validation UI", () => { await page.keyboard.up("Meta"); - const inputNode = page.getByTestId("io-node-input-label_column_name"); + const inputNode = page.locator( + '[data-tour-card="input"][data-tour-card-name="label_column_name"]', + ); await expect( inputNode, "Input node should be created for label_column_name", ).toBeVisible(); - await assertValidationState(page, "1 warning detected"); + await expectValidationState(page, "1 warning"); }); test("setting input value resolves all validation issues", async () => { - const inputNode = page.getByTestId("io-node-input-label_column_name"); + const inputNode = page.locator( + '[data-tour-card="input"][data-tour-card-name="label_column_name"]', + ); await inputNode.click(); - const inputValueField = page.getByTestId("input-value-field"); + const inputValueField = page.getByTestId("input-value"); await expect( inputValueField, "Input value editor should appear in context panel", @@ -202,6 +193,6 @@ test.describe("Pipeline Validation UI", () => { await inputValueField.fill("tips"); await inputValueField.blur(); - await assertValidationState(page, "No validation issues found"); + await expectValidationState(page, "No validation issues"); }); });
- Are you sure you want to update{" "} - {nodesToUpdate.length === 1 ? "this node" : "these nodes"}? -
- This will upgrade {nodesToUpdate.length} node - {nodeToUpdatePluralSuffix} to {nodesToUpdate.length === 1 ? "a " : ""}{" "} - new version - {nodeToUpdatePluralSuffix} from{" "} - {nodesToUpdate.length === 1 ? "its" : "their"} source URL. -
- {unchangedNodes.length} custom component - {unchangedNodes.length === 1 ? "" : "s"} will not be updated. -
Some input arguments may be lost.
{`Are you sure you want to replace "${oldTaskId}" with "${newTaskId}"?`}
The following input arguments will be removed:
All other inputs will be preserved.
All inputs will be preserved.
- This will upgrade the Task from version{" "} - - - - {oldTaskDigest?.substring(0, 8)} - - - {oldTaskDigest} - {" "} - to version{" "} - - - - {newComponentDigest?.substring(0, 8)} - - - - {newComponentDigest} - - -
- {title} -
- {content} -