diff --git a/bun.lock b/bun.lock index 9d650c50d..54d9777ee 100644 --- a/bun.lock +++ b/bun.lock @@ -45,7 +45,7 @@ "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-query": "^5.90.7", "@tanstack/react-table": "^8.20.6", - "@tanstack/react-virtual": "^3.11.3", + "@tanstack/react-virtual": "^3.13.12", "@theguild/remark-mermaid": "^0.2.0", "@trpc/client": "^11.7.1", "@trpc/react-query": "^11.7.1", @@ -66,7 +66,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "deepmerge": "^4.3.1", - "e2b": "1.10.0", + "e2b": "^2.7.0", "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", "fast-xml-parser": "^4.5.1", @@ -104,7 +104,7 @@ "styled-components": "^6.1.19", "superjson": "^2.2.5", "swr": "^2.3.4", - "tailwind-merge": "^3.3.1", + "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.3.6", "usehooks-ts": "^3.1.0", "vaul": "^1.1.2", @@ -424,8 +424,14 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], @@ -1392,6 +1398,8 @@ "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], "chrono-node": ["chrono-node@2.9.0", "", {}, "sha512-glI4YY2Jy6JII5l3d5FN6rcrIbKSQqKPhWsIRYPK2IK8Mm4Q1ZZFdYIaDqglUNf7gNwG+kWIzTn0omzzE0VkvQ=="], @@ -1580,6 +1588,8 @@ "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + "dockerfile-ast": ["dockerfile-ast@0.7.1", "", { "dependencies": { "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3" } }, "sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], @@ -1602,7 +1612,7 @@ "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], - "e2b": ["e2b@1.10.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "compare-versions": "^6.1.0", "openapi-fetch": "^0.9.7", "platform": "^1.3.6" } }, "sha512-m0lt8hTQ84M7tUjF2Dw7oNwfMcc8EyCHJtA1vX6Sv3OO2OtjPdCky854XWY+UejDK+q3m5vuSpSgLgeE0rJ7LA=="], + "e2b": ["e2b@2.7.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^11.0.3", "openapi-fetch": "^0.14.1", "platform": "^1.3.6", "tar": "^7.4.3" } }, "sha512-pbCbkkdkkY+yIhhtdSE7lM/vhIROtHNI0hNpj8lBphDILNH2qmmjhxU7/wam8/xWRbiWbfuQaOsv100lD32nag=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -1800,7 +1810,7 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -1978,7 +1988,7 @@ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], @@ -2190,6 +2200,8 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], @@ -2298,7 +2310,7 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -2644,7 +2656,7 @@ "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], - "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="], @@ -2652,6 +2664,8 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="], + "teeny-request": ["teeny-request@9.0.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="], "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], @@ -2842,7 +2856,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], @@ -3212,7 +3226,7 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "e2b/openapi-fetch": ["openapi-fetch@0.9.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.8" } }, "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg=="], + "e2b/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -3234,7 +3248,7 @@ "gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -3254,6 +3268,8 @@ "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "mermaid/stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -3278,7 +3294,7 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], @@ -3344,6 +3360,8 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "test-exclude/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], @@ -3504,12 +3522,8 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "e2b/openapi-fetch/openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.8", "", {}, "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="], - "extract-zip/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "jest-diff/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "jest-matcher-utils/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -3590,6 +3604,10 @@ "teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "test-exclude/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -3700,6 +3718,8 @@ "remark-mermaid/unist-util-visit/unist-util-visit-parents/unist-util-is": ["unist-util-is@3.0.0", "", {}, "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A=="], + "test-exclude/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "mermaid.cli/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], } } diff --git a/next.config.mjs b/next.config.mjs index 292b0fe31..a6b163d5a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -14,6 +14,16 @@ const config = { authInterrupts: true, clientSegmentCache: true, }, + turbopack: { + resolveAlias: { + // Stub Node.js modules for browser builds + // e2b package bundles these packages. when dealing with browser chunks, + // we need to stub these packages for builds. + fs: { browser: './stubs/fs.ts' }, + 'node:fs': { browser: './stubs/fs.ts' }, + 'node:fs/promises': { browser: './stubs/fs-promises.ts' }, + }, + }, logging: { fetches: { fullUrl: true, diff --git a/package.json b/package.json index 885d14b8c..b7d534581 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "scripts:create-migration": "bun scripts/create-migration.ts", "<<<<<<< Development": "", "shad": "bunx shadcn@canary", - "test:development:traffic": "vitest run src/__test__/development/traffic.test.ts", + "test:dev:traffic": "vitest run src/__test__/development/traffic.test.ts", + "test:dev:build": "vitest run src/__test__/development/template-build.test.ts", "clean": "rm -rf .next node_modules && bun install", "<<<<<<< Testing": "", "test:run": "bun scripts:check-all-env && vitest run", @@ -81,7 +82,7 @@ "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-query": "^5.90.7", "@tanstack/react-table": "^8.20.6", - "@tanstack/react-virtual": "^3.11.3", + "@tanstack/react-virtual": "^3.13.12", "@theguild/remark-mermaid": "^0.2.0", "@trpc/client": "^11.7.1", "@trpc/react-query": "^11.7.1", @@ -102,7 +103,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "deepmerge": "^4.3.1", - "e2b": "1.10.0", + "e2b": "^2.7.0", "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", "fast-xml-parser": "^4.5.1", @@ -140,7 +141,7 @@ "styled-components": "^6.1.19", "superjson": "^2.2.5", "swr": "^2.3.4", - "tailwind-merge": "^3.3.1", + "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.3.6", "usehooks-ts": "^3.1.0", "vaul": "^1.1.2", diff --git a/src/__test__/development/template-build.test.ts b/src/__test__/development/template-build.test.ts new file mode 100644 index 000000000..056edd99d --- /dev/null +++ b/src/__test__/development/template-build.test.ts @@ -0,0 +1,63 @@ +/** + * This test builds a basic sandbox template using the E2B SDK, + * useful for development and testing of template building features in the dashboard. + */ + +import { Template } from 'e2b' +import { describe, expect, it } from 'vitest' + +const l = console + +const { TEST_E2B_DOMAIN, TEST_E2B_API_KEY } = import.meta.env + +if (!TEST_E2B_DOMAIN || !TEST_E2B_API_KEY) { + throw new Error( + 'Missing environment variables: TEST_E2B_DOMAIN and/or TEST_E2B_API_KEY' + ) +} + +const BUILD_TIMEOUT_MS = 5 * 60 * 1000 + +describe('E2B Template build test', () => { + it( + 'builds a basic template with Node.js', + { timeout: BUILD_TIMEOUT_MS }, + async () => { + const templateName = `test-template-${Date.now()}` + + l.info('test:starting_template_build', { + templateName, + startTime: new Date().toISOString(), + }) + + const template = Template() + .skipCache() + .fromNodeImage('lts') + .setWorkdir('/app') + .runCmd('echo "Hello from template build"') + .setStartCmd('node --version', 'node --version') + + const buildInfo = await Template.build(template, { + alias: templateName, + apiKey: TEST_E2B_API_KEY, + domain: TEST_E2B_DOMAIN, + onBuildLogs: (log) => { + l.info('test:build_log', { + level: log.level, + message: log.message, + }) + }, + }) + + l.info('test:template_build_completed', { + templateId: buildInfo.templateId, + buildId: buildInfo.buildId, + alias: buildInfo.alias, + endTime: new Date().toISOString(), + }) + + expect(buildInfo.templateId).toBeDefined() + expect(buildInfo.buildId).toBeDefined() + } + ) +}) diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/default.tsx b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/default.tsx new file mode 100644 index 000000000..86b9e9a38 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null +} diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/page.tsx b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/page.tsx new file mode 100644 index 000000000..3e280c132 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/page.tsx @@ -0,0 +1,11 @@ +import BuildsHeader from '@/features/dashboard/templates/builds/header' +import BuildsTable from '@/features/dashboard/templates/builds/table' + +export default function BuildsPage() { + return ( +
+ + +
+ ) +} diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/default.tsx b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/default.tsx new file mode 100644 index 000000000..86b9e9a38 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null +} diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/page.tsx b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/page.tsx new file mode 100644 index 000000000..5900c4cf3 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/page.tsx @@ -0,0 +1,11 @@ +import LoadingLayout from '@/features/dashboard/loading-layout' +import TemplatesTable from '@/features/dashboard/templates/list/table' +import { Suspense } from 'react' + +export default async function ListPage() { + return ( + }> + + + ) +} diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/layout.tsx b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/layout.tsx new file mode 100644 index 000000000..4e7debefb --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/layout.tsx @@ -0,0 +1,33 @@ +import { DashboardTab, DashboardTabs } from '@/ui/dashboard-tabs' +import { BuildIcon, ListIcon } from '@/ui/primitives/icons' + +export default function TemplatesLayout({ + list, + builds, +}: LayoutProps<'/dashboard/[teamIdOrSlug]/templates'> & { + list: React.ReactNode + builds: React.ReactNode +}) { + return ( + + } + > + {list} + + } + > + {builds} + + + ) +} diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/page.tsx b/src/app/dashboard/[teamIdOrSlug]/templates/page.tsx deleted file mode 100644 index ea21637fc..000000000 --- a/src/app/dashboard/[teamIdOrSlug]/templates/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import LoadingLayout from '@/features/dashboard/loading-layout' -import TemplatesTable from '@/features/dashboard/templates/table' -import { HydrateClient, prefetch, trpc } from '@/trpc/server' -import { Suspense } from 'react' - -export default async function Page({ - params, -}: PageProps<'/dashboard/[teamIdOrSlug]/templates'>) { - const { teamIdOrSlug } = await params - - prefetch( - trpc.templates.getTemplates.queryOptions({ - teamIdOrSlug, - }) - ) - prefetch(trpc.templates.getDefaultTemplatesCached.queryOptions()) - - return ( - - }> - - - - ) -} diff --git a/src/configs/urls.ts b/src/configs/urls.ts index 7966ea1e5..ed4af2c4a 100644 --- a/src/configs/urls.ts +++ b/src/configs/urls.ts @@ -35,6 +35,11 @@ export const PROTECTED_URLS = { WEBHOOKS: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/webhooks`, TEMPLATES: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/templates`, + TEMPLATES_LIST: (teamIdOrSlug: string) => + `/dashboard/${teamIdOrSlug}/templates?tab=list`, + TEMPLATES_BUILDS: (teamIdOrSlug: string) => + `/dashboard/${teamIdOrSlug}/templates?tab=builds`, + USAGE: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/usage`, BILLING: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/billing`, BUDGET: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/budget`, diff --git a/src/features/dashboard/layout/wrapper.tsx b/src/features/dashboard/layout/wrapper.tsx index ba0925305..6473d5317 100644 --- a/src/features/dashboard/layout/wrapper.tsx +++ b/src/features/dashboard/layout/wrapper.tsx @@ -4,31 +4,32 @@ import { getDashboardLayoutConfig } from '@/configs/layout' import { CatchErrorBoundary } from '@/ui/error' import { usePathname } from 'next/navigation' -export default function DashboardLayoutWrapper({ +export function DefaultDashboardLayout({ children, }: { children: React.ReactNode }) { - const pathname = usePathname() - const config = getDashboardLayoutConfig(pathname) - - if (config.type === 'default') { - return ( -
-
- - {children} - -
+ return ( +
+
+ + {children} +
- ) - } +
+ ) +} +export function CustomDashboardLayout({ + children, +}: { + children: React.ReactNode +}) { return (
) } + +export default function DashboardLayoutWrapper({ + children, +}: { + children: React.ReactNode +}) { + const pathname = usePathname() + const config = getDashboardLayoutConfig(pathname) + + if (config.type === 'default') { + return {children} + } + + return {children} +} diff --git a/src/features/dashboard/loading-layout.tsx b/src/features/dashboard/loading-layout.tsx index b916cd40c..a0d073ffc 100644 --- a/src/features/dashboard/loading-layout.tsx +++ b/src/features/dashboard/loading-layout.tsx @@ -1,9 +1,11 @@ -import { Loader } from '@/ui/primitives/loader_d' +'use client' + +import { Loader } from '@/ui/primitives/loader' export default function LoadingLayout() { return (
- +
) } diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 1359ed0b6..b80283bf5 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -37,7 +37,7 @@ interface SandboxInspectProviderProps { seedEntries?: EntryInfo[] } -export function SandboxInspectProvider({ +export default function SandboxInspectProvider({ children, rootPath, seedEntries, @@ -232,8 +232,7 @@ export function SandboxInspectProvider({ sandboxManagerRef.current = new SandboxManager( storeRef.current, sandbox, - rootPath, - sandboxInfo.envdAccessToken !== undefined + rootPath ) trackInteraction('started_watching', { diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 1aa5364a0..974ad4941 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -25,7 +25,6 @@ export class SandboxManager { private readonly rootPath: string private store: FilesystemStore private sandbox: Sandbox - private readonly isSandboxSecure: boolean = false private static readonly LOAD_DEBOUNCE_MS = 250 private static readonly READ_DEBOUNCE_MS = 250 @@ -50,16 +49,10 @@ export class SandboxManager { } > = new Map() - constructor( - store: FilesystemStore, - sandbox: Sandbox, - rootPath: string, - isSandboxSecure: boolean - ) { + constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) { this.store = store this.sandbox = sandbox this.rootPath = normalizePath(rootPath) - this.isSandboxSecure = isSandboxSecure // immediately start a single recursive watcher at the root void this.startRootWatcher() @@ -360,11 +353,8 @@ export class SandboxManager { const downloadUrl = await this.sandbox.downloadUrl(normalizedPath, { user: 'root', - useSignature: this.isSandboxSecure || undefined, }) - console.log('downloadUrl', downloadUrl) - return downloadUrl } diff --git a/src/features/dashboard/sandbox/inspect/view.tsx b/src/features/dashboard/sandbox/inspect/view.tsx index 83de3254a..ccc3f9867 100644 --- a/src/features/dashboard/sandbox/inspect/view.tsx +++ b/src/features/dashboard/sandbox/inspect/view.tsx @@ -1,6 +1,6 @@ 'use client' -import { SandboxInspectProvider } from '@/features/dashboard/sandbox/inspect/context' +import SandboxInspectProvider from '@/features/dashboard/sandbox/inspect/context' import SandboxInspectFilesystem from '@/features/dashboard/sandbox/inspect/filesystem' import SandboxInspectViewer from '@/features/dashboard/sandbox/inspect/viewer' import { cn } from '@/lib/utils' diff --git a/src/features/dashboard/sandboxes/list/table-cells.tsx b/src/features/dashboard/sandboxes/list/table-cells.tsx index 36d66b520..5bae0c898 100644 --- a/src/features/dashboard/sandboxes/list/table-cells.tsx +++ b/src/features/dashboard/sandboxes/list/table-cells.tsx @@ -2,7 +2,7 @@ import { PROTECTED_URLS } from '@/configs/urls' import ResourceUsage from '@/features/dashboard/common/resource-usage' -import { useTemplateTableStore } from '@/features/dashboard/templates/stores/table-store' +import { useTemplateTableStore } from '@/features/dashboard/templates/list/stores/table-store' import { defaultErrorToast, defaultSuccessToast, diff --git a/src/features/dashboard/templates/builds/constants.ts b/src/features/dashboard/templates/builds/constants.ts new file mode 100644 index 000000000..c7bd04dca --- /dev/null +++ b/src/features/dashboard/templates/builds/constants.ts @@ -0,0 +1,7 @@ +import { BuildStatusDTO } from '@/server/api/models/builds.models' + +export const INITIAL_BUILD_STATUSES: BuildStatusDTO[] = [ + 'building', + 'failed', + 'success', +] diff --git a/src/features/dashboard/templates/builds/empty.tsx b/src/features/dashboard/templates/builds/empty.tsx new file mode 100644 index 000000000..e4e643463 --- /dev/null +++ b/src/features/dashboard/templates/builds/empty.tsx @@ -0,0 +1,24 @@ +import { cn } from '@/lib/utils' +import { BuildIcon } from '@/ui/primitives/icons' + +interface BuildsEmptyProps { + error?: string +} + +export default function BuildsEmpty({ error }: BuildsEmptyProps) { + return ( +
+ +

+ {error ? error : 'No template builds found'} +

+
+ ) +} diff --git a/src/features/dashboard/templates/builds/filter-params.ts b/src/features/dashboard/templates/builds/filter-params.ts new file mode 100644 index 000000000..05acc834d --- /dev/null +++ b/src/features/dashboard/templates/builds/filter-params.ts @@ -0,0 +1,17 @@ +import { + createLoader, + parseAsArrayOf, + parseAsString, + parseAsStringEnum, +} from 'nuqs/server' + +export const templateBuildsFilterParams = { + statuses: parseAsArrayOf( + parseAsStringEnum(['building', 'failed', 'success']) + ), + buildIdOrTemplate: parseAsString, +} + +export const loadTemplateBuildsFilters = createLoader( + templateBuildsFilterParams +) diff --git a/src/features/dashboard/templates/builds/header.tsx b/src/features/dashboard/templates/builds/header.tsx new file mode 100644 index 000000000..f827cddbf --- /dev/null +++ b/src/features/dashboard/templates/builds/header.tsx @@ -0,0 +1,150 @@ +'use client' + +import { cn } from '@/lib/utils' +import type { BuildStatusDTO } from '@/server/api/models/builds.models' +import { Button } from '@/ui/primitives/button' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/ui/primitives/dropdown-menu' +import { Input } from '@/ui/primitives/input' +import { useEffect, useState } from 'react' +import { Status } from './table-cells' +import useFilters from './use-filters' + +interface DashedStatusCircleIconProps { + status: BuildStatusDTO + index: number +} + +const DashedStatusCircleIcon = ({ + status, + index, +}: DashedStatusCircleIconProps) => { + return ( +
+ ) +} + +const StatusIcons = ({ + selectedStatuses, +}: { + selectedStatuses: BuildStatusDTO[] +}) => { + const statusOrder: BuildStatusDTO[] = ['building', 'failed', 'success'] + const sortedStatuses = statusOrder.filter((s) => selectedStatuses.includes(s)) + + return ( +
+ {sortedStatuses.map((status, i) => ( + + ))} +
+ ) +} + +const STATUS_OPTIONS: Array<{ value: BuildStatusDTO; label: string }> = [ + { value: 'building', label: 'Building' }, + { value: 'success', label: 'Success' }, + { value: 'failed', label: 'Failed' }, +] + +export default function BuildsHeader() { + const { statuses, setStatuses, buildIdOrTemplate, setBuildIdOrTemplate } = + useFilters() + + const [localBuildIdOrTemplate, setLocalBuildIdOrTemplate] = useState( + buildIdOrTemplate ?? '' + ) + + const [localStatuses, setLocalStatuses] = useState(statuses) + + useEffect(() => { + setLocalBuildIdOrTemplate(buildIdOrTemplate ?? '') + }, [buildIdOrTemplate]) + + useEffect(() => { + setLocalStatuses(statuses) + }, [statuses]) + + const toggleStatus = (status: BuildStatusDTO) => { + const isSelected = localStatuses.includes(status) + + if (isSelected && localStatuses.length === 1) { + return + } + + const newStatuses = isSelected + ? localStatuses.filter((s) => s !== status) + : [...localStatuses, status] + + setLocalStatuses(newStatuses) + setStatuses(newStatuses) + } + + const selectAllStatuses = () => { + const allStatuses = STATUS_OPTIONS.map((s) => s.value) + setLocalStatuses(allStatuses) + setStatuses(allStatuses) + } + + return ( +
+ { + setLocalBuildIdOrTemplate(e.target.value) + setBuildIdOrTemplate(e.target.value) + }} + /> + + + + + + + e.preventDefault()} + > + All + + + {STATUS_OPTIONS.map((option) => ( + toggleStatus(option.value)} + onSelect={(e) => e.preventDefault()} + > + + + ))} + + +
+ ) +} diff --git a/src/features/dashboard/templates/builds/table-cells.tsx b/src/features/dashboard/templates/builds/table-cells.tsx new file mode 100644 index 000000000..e6fa1a83a --- /dev/null +++ b/src/features/dashboard/templates/builds/table-cells.tsx @@ -0,0 +1,242 @@ +'use client' + +import { PROTECTED_URLS } from '@/configs/urls' +import { useTemplateTableStore } from '@/features/dashboard/templates/list/stores/table-store' +import { useClipboard } from '@/lib/hooks/use-clipboard' +import { cn } from '@/lib/utils' +import { + formatDurationCompact, + formatTimeAgoCompact, +} from '@/lib/utils/formatting' +import type { + BuildStatusDTO, + ListedBuildDTO, +} from '@/server/api/models/builds.models' +import { Badge } from '@/ui/primitives/badge' +import { Button } from '@/ui/primitives/button' +import { CheckIcon, CloseIcon } from '@/ui/primitives/icons' +import { Loader } from '@/ui/primitives/loader' +import { ArrowUpRight } from 'lucide-react' +import { useParams, useRouter } from 'next/navigation' +import { useEffect, useState } from 'react' + +function CopyableCell({ + value, + children, + className, +}: { + value: string + children: React.ReactNode + className?: string +}) { + const [wasCopied, copy] = useClipboard() + + return ( + + ) +} + +export function BuildId({ id }: { id: string }) { + return ( + + {id} + + ) +} + +export function Template({ + name, + templateId, +}: { + name: string + templateId: string +}) { + const router = useRouter() + const { teamIdOrSlug } = + useParams< + Awaited['params']> + >() + + return ( + + ) +} + +export function LoadMoreButton({ + isLoading, + onLoadMore, +}: { + isLoading: boolean + onLoadMore: () => void +}) { + if (isLoading) { + return ( + + + Loading... + + ) + } + return ( + + ) +} + +export function BackToTopButton({ onBackToTop }: { onBackToTop: () => void }) { + return ( + + ) +} + +export function Duration({ + createdAt, + finishedAt, + isBuilding, +}: { + createdAt: number + finishedAt: number | null + isBuilding: boolean +}) { + const [now, setNow] = useState(() => Date.now()) + + useEffect(() => { + if (!isBuilding) return + + const interval = setInterval(() => { + setNow(Date.now()) + }, 1000) + + return () => clearInterval(interval) + }, [isBuilding]) + + const duration = isBuilding + ? now - createdAt + : (finishedAt ?? now) - createdAt + const iso = finishedAt ? new Date(finishedAt).toISOString() : null + + if (isBuilding || !iso) { + return ( + + {formatDurationCompact(duration)} + + ) + } + + return ( + + {formatDurationCompact(duration)} + + ) +} + +export function StartedAt({ timestamp }: { timestamp: number }) { + const iso = new Date(timestamp).toISOString() + const elapsed = Date.now() - timestamp + + return ( + + {formatTimeAgoCompact(elapsed)} + + ) +} + +interface StatusProps { + status: BuildStatusDTO +} + +export function Status({ status }: StatusProps) { + const config: Record< + BuildStatusDTO, + { + label: string + variant: 'default' | 'positive' | 'error' + icon: React.ReactNode + } + > = { + building: { + label: 'Building', + variant: 'default', + icon: null, + }, + success: { + label: 'Success', + variant: 'positive', + icon: , + }, + failed: { + label: 'Failed', + variant: 'error', + icon: , + }, + } + + const { label, icon, variant } = config[status] + + return ( +
+ + {icon} + {label} + +
+ ) +} + +export function Reason({ + statusMessage, +}: { + statusMessage: ListedBuildDTO['statusMessage'] +}) { + if (!statusMessage) return null + + return ( + + {statusMessage} + + ) +} diff --git a/src/features/dashboard/templates/builds/table.tsx b/src/features/dashboard/templates/builds/table.tsx new file mode 100644 index 000000000..db1e9604d --- /dev/null +++ b/src/features/dashboard/templates/builds/table.tsx @@ -0,0 +1,341 @@ +'use client' + +import type { + ListedBuildDTO, + RunningBuildStatusDTO, +} from '@/server/api/models/builds.models' +import { useTRPC } from '@/trpc/client' +import { ArrowDownIcon } from '@/ui/primitives/icons' +import { Loader } from '@/ui/primitives/loader' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/ui/primitives/table' +import { + keepPreviousData, + useInfiniteQuery, + useQuery, + useQueryClient, +} from '@tanstack/react-query' +import { useParams } from 'next/navigation' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import BuildsEmpty from './empty' +import { + BackToTopButton, + BuildId, + Duration, + LoadMoreButton, + Reason, + StartedAt, + Status, + Template, +} from './table-cells' +import useFilters from './use-filters' + +const BUILDS_REFETCH_INTERVAL_MS = 15_000 +const RUNNING_BUILD_POLL_INTERVAL_MS = 3_000 +const MAX_CACHED_PAGES = 3 + +const COLUMN_WIDTHS = { + id: 132, + status: 96, + template: 192, + started: 156, + duration: 96, +} as const + +const BuildsTable = () => { + const trpc = useTRPC() + const queryClient = useQueryClient() + const scrollContainerRef = useRef(null) + + const { teamIdOrSlug } = + useParams< + Awaited['params']> + >() + const { statuses, buildIdOrTemplate } = useFilters() + const { isFilterRefetching, clearFilterRefetching } = useFilterChangeTracking( + statuses, + buildIdOrTemplate + ) + + // Builds list query + const { + data: paginatedBuilds, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isFetching: isFetchingBuilds, + isPending: isInitialLoad, + error: buildsError, + } = useInfiniteQuery( + trpc.builds.list.infiniteQueryOptions( + { teamIdOrSlug, statuses, buildIdOrTemplate }, + { + getNextPageParam: (page) => page.nextCursor ?? undefined, + placeholderData: keepPreviousData, + retry: 3, + refetchInterval: BUILDS_REFETCH_INTERVAL_MS, + refetchIntervalInBackground: false, + maxPages: MAX_CACHED_PAGES, + } + ) + ) + + const builds = useMemo( + () => paginatedBuilds?.pages.flatMap((p) => p.data) ?? [], + [paginatedBuilds] + ) + + const hasScrolledPastInitialPages = + paginatedBuilds?.pageParams[0] !== undefined + + useEffect(() => { + if (!isFetchingBuilds && isFilterRefetching) { + clearFilterRefetching() + } + }, [isFetchingBuilds, isFilterRefetching, clearFilterRefetching]) + + // Running builds status polling + const runningBuildIds = useMemo( + () => builds.filter((b) => b.status === 'building').map((b) => b.id), + [builds] + ) + + const { data: runningStatusesData } = useQuery( + trpc.builds.runningStatuses.queryOptions( + { teamIdOrSlug, buildIds: runningBuildIds }, + { + enabled: runningBuildIds.length > 0, + refetchInterval: (query) => { + const hasRunningBuilds = query.state.data?.some( + (s) => s.status === 'building' + ) + return hasRunningBuilds ? RUNNING_BUILD_POLL_INTERVAL_MS : false + }, + refetchIntervalInBackground: false, + refetchOnWindowFocus: 'always', + retry: false, + } + ) + ) + + const buildsWithLiveStatus = useMemo( + () => mergeBuildsWithLiveStatuses(builds, runningStatusesData), + [builds, runningStatusesData] + ) + + // Handlers + const buildsQueryKey = trpc.builds.list.infiniteQueryOptions({ + teamIdOrSlug, + statuses, + buildIdOrTemplate, + }).queryKey + + const handleLoadMore = useCallback(() => { + fetchNextPage() + }, [fetchNextPage]) + + const handleBackToTop = useCallback(() => { + queryClient.resetQueries({ queryKey: buildsQueryKey }) + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTop = 0 + } + }, [queryClient, buildsQueryKey]) + + // Derived UI state + const hasData = buildsWithLiveStatus.length > 0 + const showLoader = isInitialLoad && !hasData + const showEmpty = !isInitialLoad && !isFetchingBuilds && !hasData + const showFilterRefetchingOverlay = isFilterRefetching && hasData + + return ( +
+
+ + + + + + + + + + + + + Build ID + Status + Template + + + Started + + + + Duration +
+ + + + + {showLoader && ( + + +
+ +
+
+
+ )} + + {showEmpty && ( + + + + + + )} + + {hasData && ( + <> + {hasScrolledPastInitialPages && ( + + + + + + )} + + {buildsWithLiveStatus.map((build) => { + const isBuilding = build.status === 'building' + + return ( + + + + + + + + +