From 03ce67b999bb57a561e7147b4a592588d30bfa58 Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Mon, 30 Sep 2024 19:26:05 +0100 Subject: [PATCH] feat: added environments and launch functionality to projects (#87) * feat: add environments schema definition Signed-off-by: Francisco Madeira * feat: remove query from getProjects action * refactor: remove query from getProjects * refactor: remove query from getProjectsById * refactor: fix projects/{id} with fixed environtment * refactor: project components list and add * refactor: project components actions * feat: add environment picker * feat: add environment picker to projects page Signed-off-by: Francisco Madeira * refactor: component actions with environments * refactor: api routes for projects * feat: add new environment based api routes * feat: add copy url to environments * feat: actions on project environments * refactor: clean types on projects actions * feat: add default environment to new projects * feat: create launch page Signed-off-by: Francisco Madeira * feat: added launch funcionality * feat: improve comparison page result Signed-off-by: Francisco Madeira * chore: add changeset Signed-off-by: Francisco Madeira * fix: remove group to display all the components Signed-off-by: Francisco Madeira * chore: update the release workflow Signed-off-by: Francisco Madeira --------- Signed-off-by: Francisco Madeira --- .changeset/clever-fireants-walk.md | 13 + .changeset/tiny-mayflies-yawn.md | 2 +- .github/workflows/astro.yml | 10 +- .github/workflows/dashboard-preview.yml | 1 + .github/workflows/release.yml | 48 ++ examples/next-mf/home/next.config.js | 29 - examples/next-mf/home/package.json | 18 - examples/next-mf/home/pages/_app.js | 32 - examples/next-mf/home/pages/_document.js | 63 -- examples/next-mf/home/pages/index.js | 45 -- examples/next-mf/home/public/favicon.ico | Bin 15086 -> 0 bytes examples/next-mf/package.json | 17 - examples/next-mf/remote/components/Button.js | 10 - examples/next-mf/remote/next.config.js | 22 - examples/next-mf/remote/package.json | 18 - examples/next-mf/remote/pages/_app.js | 8 - examples/next-mf/remote/pages/_document.js | 20 - examples/next-mf/remote/pages/index.js | 7 - examples/next-mf/remote/public/favicon.ico | Bin 15086 -> 0 bytes .../ReactHelloWorld/ReactHelloWorld.tsx | 2 +- package.json | 4 +- .../[id]/versions/[versionId]/[tab]/page.tsx | 59 +- .../src/app/(session)/projects/[id]/page.tsx | 17 +- .../projects/launch/new/[id]/page.tsx | 87 +++ .../projects/launch/new/[id]/picker.tsx | 103 +++ .../[id]/components/[name]/route.ts | 92 +++ .../[id]/components/[name]/ssr/route.ts | 112 +++ .../v1/environments/[id]/components/route.ts | 84 +++ .../components/component/version/tabs.tsx | 6 +- .../component/version/tabs/dependents.tsx | 134 ++-- .../src/components/launches/table/columns.tsx | 81 ++ .../launches/table/launch-button.tsx | 75 ++ .../launches/table/launches-list.tsx | 32 + .../component-selection-table/actions.tsx | 4 +- .../active-switch.tsx | 5 +- .../component-selection-table/columns.tsx | 6 +- .../components-dialog.tsx | 191 ++++- .../components-list.tsx | 34 +- .../version-picker.tsx | 7 +- .../components/projects/environments-form.tsx | 113 +++ .../projects/environments-table/actions.tsx | 96 +++ .../environments-table/active-switch.tsx | 56 ++ .../projects/environments-table/columns.tsx | 59 ++ .../environments-table/environment-dialog.tsx | 44 ++ .../environments-table/environment-list.tsx | 29 + .../projects/members-table/columns.tsx | 3 +- .../src/components/projects/table/columns.tsx | 13 + .../projects/table/data-table-row-actions.tsx | 77 +- web/dashboard/src/components/ui/badge.tsx | 7 +- .../components/ui/update-projects-view.tsx | 1 - web/dashboard/src/data/components/actions.ts | 636 ++++++++-------- web/dashboard/src/data/components/schema.ts | 6 +- web/dashboard/src/data/events/actions.ts | 4 +- web/dashboard/src/data/launches/actions.ts | 82 ++ web/dashboard/src/data/member/actions.ts | 2 +- web/dashboard/src/data/projects/actions.ts | 708 +++++++++++++----- web/dashboard/src/data/projects/dto.ts | 67 +- web/dashboard/src/data/projects/schema.ts | 40 +- web/dashboard/src/data/users/actions.ts | 4 +- web/site/package.json | 2 +- .../components/starlight/CallToAction.astro | 6 +- web/site/src/components/starlight/Hero.astro | 11 +- 62 files changed, 2509 insertions(+), 1055 deletions(-) create mode 100644 .changeset/clever-fireants-walk.md create mode 100644 .github/workflows/release.yml delete mode 100644 examples/next-mf/home/next.config.js delete mode 100644 examples/next-mf/home/package.json delete mode 100644 examples/next-mf/home/pages/_app.js delete mode 100644 examples/next-mf/home/pages/_document.js delete mode 100644 examples/next-mf/home/pages/index.js delete mode 100644 examples/next-mf/home/public/favicon.ico delete mode 100644 examples/next-mf/package.json delete mode 100644 examples/next-mf/remote/components/Button.js delete mode 100644 examples/next-mf/remote/next.config.js delete mode 100644 examples/next-mf/remote/package.json delete mode 100644 examples/next-mf/remote/pages/_app.js delete mode 100644 examples/next-mf/remote/pages/_document.js delete mode 100644 examples/next-mf/remote/pages/index.js delete mode 100644 examples/next-mf/remote/public/favicon.ico create mode 100644 web/dashboard/src/app/(session)/projects/launch/new/[id]/page.tsx create mode 100644 web/dashboard/src/app/(session)/projects/launch/new/[id]/picker.tsx create mode 100644 web/dashboard/src/app/api/v1/environments/[id]/components/[name]/route.ts create mode 100644 web/dashboard/src/app/api/v1/environments/[id]/components/[name]/ssr/route.ts create mode 100644 web/dashboard/src/app/api/v1/environments/[id]/components/route.ts create mode 100644 web/dashboard/src/components/launches/table/columns.tsx create mode 100644 web/dashboard/src/components/launches/table/launch-button.tsx create mode 100644 web/dashboard/src/components/launches/table/launches-list.tsx create mode 100644 web/dashboard/src/components/projects/environments-form.tsx create mode 100644 web/dashboard/src/components/projects/environments-table/actions.tsx create mode 100644 web/dashboard/src/components/projects/environments-table/active-switch.tsx create mode 100644 web/dashboard/src/components/projects/environments-table/columns.tsx create mode 100644 web/dashboard/src/components/projects/environments-table/environment-dialog.tsx create mode 100644 web/dashboard/src/components/projects/environments-table/environment-list.tsx create mode 100644 web/dashboard/src/data/launches/actions.ts diff --git a/.changeset/clever-fireants-walk.md b/.changeset/clever-fireants-walk.md new file mode 100644 index 00000000..d0ab414e --- /dev/null +++ b/.changeset/clever-fireants-walk.md @@ -0,0 +1,13 @@ +--- +'@ethereal-nexus/dashboard': major +'@ethereal-nexus/site': major +'@ethereal-nexus/cli': major +'@ethereal-nexus/conector-aem-react': major +'@ethereal-nexus/core': major +'@ethereal-nexus/vite-plugin-ethereal-nexus': major +--- + +This is the first Major release of Ethereal Nexus! + +Added environments features. This allows projects to have several configurations based on the respective environemnt and to publish them from one environment to the other using launches. +Because of this there were breaking changes to the schema of the component configs. diff --git a/.changeset/tiny-mayflies-yawn.md b/.changeset/tiny-mayflies-yawn.md index 13dc11ff..7dce47ac 100644 --- a/.changeset/tiny-mayflies-yawn.md +++ b/.changeset/tiny-mayflies-yawn.md @@ -2,4 +2,4 @@ '@ethereal-nexus/core': patch --- -fix type inference for select schema +Fix type inference for select schema diff --git a/.github/workflows/astro.yml b/.github/workflows/astro.yml index 21d49212..03e15f9e 100644 --- a/.github/workflows/astro.yml +++ b/.github/workflows/astro.yml @@ -39,25 +39,29 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Detect package manager id: detect-package-manager run: | echo "manager=pnpm" >> $GITHUB_OUTPUT echo "command=install" >> $GITHUB_OUTPUT echo "runner=pnpm run" >> $GITHUB_OUTPUT + - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 20 + - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 8 run_install: false + - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 name: Setup pnpm cache with: @@ -65,18 +69,22 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Setup Pages id: pages uses: actions/configure-pages@v4 + - name: Install dependencies run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} working-directory: ${{ env.BUILD_PATH }} + - name: Build with Astro run: | ${{ steps.detect-package-manager.outputs.runner }} astro build \ --site "${{ steps.pages.outputs.origin }}" \ --base "${{ steps.pages.outputs.base_path }}" working-directory: ${{ env.BUILD_PATH }} + - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/.github/workflows/dashboard-preview.yml b/.github/workflows/dashboard-preview.yml index 51c480a8..1021e894 100644 --- a/.github/workflows/dashboard-preview.yml +++ b/.github/workflows/dashboard-preview.yml @@ -43,6 +43,7 @@ jobs: echo "${{ secrets.AZURE_BLOB_STORAGE_ACCOUNT }}" >> ./web/dashboard/.env echo "${{ secrets.AZURE_BLOB_STORAGE_SECRET }}" >> ./web/dashboard/.env echo "${{ secrets.NEXT_AUTH_SECRET }}" >> ./web/dashboard/.env + echo "AZURE_CONTAINER_NAME=remote-components-aem-demo" >> ./web/dashboard/.env echo "DRIZZLE_DATABASE_TYPE=neon" >> ./web/dashboard/.env echo "DRIZZLE_DATABASE_URL=${{ steps.create-branch.outputs.db_url }}?sslmode=require" >> ./web/dashboard/.env diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..866c4380 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Install Dependencies + run: pnpm i --frozen-lockfile + + - name: Build Packages + run: pnpm run build + + - name: Fix npmrc + run: npm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN" + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish to npm + id: changesets + uses: changesets/action@v1 + with: + publish: pnpm release + commit: "chore: release version" + title: "[ci] Release version" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/examples/next-mf/home/next.config.js b/examples/next-mf/home/next.config.js deleted file mode 100644 index 2f64097f..00000000 --- a/examples/next-mf/home/next.config.js +++ /dev/null @@ -1,29 +0,0 @@ -const NextFederationPlugin = require('@module-federation/nextjs-mf'); -// this enables you to use import() and the webpack parser -// loading remotes on demand, not ideal for SSR -const remotes = isServer => { - const location = isServer ? 'ssr' : 'chunks'; - return { - home: `home@http://localhost:3001/_next/static/${location}/remoteEntry.js`, - shop: `shop@http://localhost:3002/_next/static/${location}/remoteEntry.js`, - }; -}; - -module.exports = { - webpack(config, options) { - config.plugins.push( - new NextFederationPlugin({ - name: 'home', - filename: 'static/chunks/remoteEntry.js', - remotes: remotes(options.isServer), - exposes: {}, - shared: {}, - extraOptions:{ - exposePages: true - } - }), - ); - - return config; - }, -}; diff --git a/examples/next-mf/home/package.json b/examples/next-mf/home/package.json deleted file mode 100644 index f2e76981..00000000 --- a/examples/next-mf/home/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "nextjs-dynamic-ssr_home", - "version": "0.1.0", - "scripts": { - "dev": "rm -rf .next;NEXT_PRIVATE_LOCAL_WEBPACK=true next dev -p 3001", - "build": "NEXT_PRIVATE_LOCAL_WEBPACK=true next build", - "start": "NEXT_PRIVATE_LOCAL_WEBPACK=true NODE_ENV=production next start -p 3001" - }, - "dependencies": { - "@module-federation/nextjs-mf": "^8.3.5", - "@module-federation/runtime": "^0.1.4", - "lodash": "4.17.21", - "next": "^14.0.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "webpack": "^5.91.0" - } -} \ No newline at end of file diff --git a/examples/next-mf/home/pages/_app.js b/examples/next-mf/home/pages/_app.js deleted file mode 100644 index d0383753..00000000 --- a/examples/next-mf/home/pages/_app.js +++ /dev/null @@ -1,32 +0,0 @@ -import App from 'next/app'; -import {init} from '@module-federation/runtime' - -const remotes = isServer => { - const location = isServer ? 'ssr' : 'chunks'; - return [ - { - name: 'remote', - entry:`http://localhost:3002/_next/static/${location}/remoteEntry.js` - }, - ]; -}; - -init({ - name: 'home', - remotes: remotes(typeof window === 'undefined'), - force: true -}) - -function MyApp({ Component, pageProps }) { - return ( - <> - - - ); -} - -MyApp.getInitialProps = async ctx => { - const appProps = await App.getInitialProps(ctx); - return appProps; -}; -export default MyApp; diff --git a/examples/next-mf/home/pages/_document.js b/examples/next-mf/home/pages/_document.js deleted file mode 100644 index abff407f..00000000 --- a/examples/next-mf/home/pages/_document.js +++ /dev/null @@ -1,63 +0,0 @@ -import Document, { Html, Head, Main, NextScript } from "next/document"; -import React from "react"; -import { revalidate, FlushedChunks, flushChunks } from "@module-federation/nextjs-mf/utils"; -import {init, loadRemote} from '@module-federation/runtime' - -class MyDocument extends Document { - static async getInitialProps(ctx) { - const remotes = isServer => { - const location = isServer ? 'ssr' : 'chunks'; - return [ - { - name: 'remote', - entry:`http://localhost:3002/_next/static/${location}/remoteEntry.js` - } - ]; - }; - - init({ - name: 'home', - remotes: remotes(typeof window === 'undefined'), - force: true - }) - if(process.env.NODE_ENV === "development" && !ctx.req.url.includes("_next")) { - await revalidate().then((shouldReload) =>{ - if (shouldReload) { - ctx.res.writeHead(302, { Location: ctx.req.url }); - ctx.res.end(); - } - }); - } else { - ctx?.res?.on("finish", () => { - revalidate() - }); - } - const initialProps = await Document.getInitialProps(ctx); - const chunks = await flushChunks() - - return { - ...initialProps, - chunks - }; - - } - - render() { - - return ( - - - - - - - -
- - - - ); - } -} - -export default MyDocument; diff --git a/examples/next-mf/home/pages/index.js b/examples/next-mf/home/pages/index.js deleted file mode 100644 index 0eda2992..00000000 --- a/examples/next-mf/home/pages/index.js +++ /dev/null @@ -1,45 +0,0 @@ -import React, {Fragment, Suspense,lazy} from 'react'; -import Head from 'next/head'; -import {init, loadRemote} from '@module-federation/runtime' - -const Home = ({loaded}) => { - const remotes = isServer => { - const location = isServer ? 'ssr' : 'chunks'; - return [ - { - name: 'remote', - entry:`http://localhost:3002/_next/static/${location}/remoteEntry.js` - }, - ]; - }; - - init({ - name: 'home', - remotes: remotes(typeof window === 'undefined'), - force: true - }) - - const RemoteButton = lazy(() => loadRemote('remote/Button')); - - return ( -
- - Home - - - -
- Remote button: - - - -
-
- ); -}; -// -Home.getInitialProps = async ctx => { - return {}; -}; - -export default Home; diff --git a/examples/next-mf/home/public/favicon.ico b/examples/next-mf/home/public/favicon.ico deleted file mode 100644 index 4965832f2c9b0605eaa189b7c7fb11124d24e48a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeHOOH5Q(7(R0cc?bh2AT>N@1PWL!LLfZKyG5c!MTHoP7_p!sBz0k$?pjS;^lmgJ zU6^i~bWuZYHL)9$wuvEKm~qo~(5=Lvx5&Hv;?X#m}i|`yaGY4gX+&b>tew;gcnRQA1kp zBbm04SRuuE{Hn+&1wk%&g;?wja_Is#1gKoFlI7f`Gt}X*-nsMO30b_J@)EFNhzd1QM zdH&qFb9PVqQOx@clvc#KAu}^GrN`q5oP(8>m4UOcp`k&xwzkTio*p?kI4BPtIwX%B zJN69cGsm=x90<;Wmh-bs>43F}ro$}Of@8)4KHndLiR$nW?*{Rl72JPUqRr3ta6e#A z%DTEbi9N}+xPtd1juj8;(CJt3r9NOgb>KTuK|z7!JB_KsFW3(pBN4oh&M&}Nb$Ee2 z$-arA6a)CdsPj`M#1DS>fqj#KF%0q?w50GN4YbmMZIoF{e1yTR=4ablqXHBB2!`wM z1M1ke9+<);|AI;f=2^F1;G6Wfpql?1d5D4rMr?#f(=hkoH)U`6Gb)#xDLjoKjp)1;Js@2Iy5yk zMXUqj+gyk1i0yLjWS|3sM2-1ECc;MAz<4t0P53%7se$$+5Ex`L5TQO_MMXXi04UDIU+3*7Ez&X|mj9cFYBXqM{M;mw_ zpw>azP*qjMyNSD4hh)XZt$gqf8f?eRSFX8VQ4Y+H3jAtvyTrXr`qHAD6`m;aYmH2zOhJC~_*AuT} zvUxC38|JYN94i(05R)dVKgUQF$}#cxV7xZ4FULqFCNX*Forhgp*yr6;DsIk=ub0Hv zpk2L{9Q&|uI^b<6@i(Y+iSxeO_n**4nRLc`P!3ld5jL=nZRw6;DEJ*1z6Pvg+eW|$lnnjO zjd|8>6l{i~UxI244CGn2kK@cJ|#ecwgSyt&HKA2)z zrOO{op^o*- { - useEffect(() => { - console.log('hooks work'); - }, []); - return ; -}; - -export default Button; diff --git a/examples/next-mf/remote/next.config.js b/examples/next-mf/remote/next.config.js deleted file mode 100644 index 53183c17..00000000 --- a/examples/next-mf/remote/next.config.js +++ /dev/null @@ -1,22 +0,0 @@ -const NextFederationPlugin = require('@module-federation/nextjs-mf'); - -module.exports = { - webpack(config, options) { - config.plugins.push( - new NextFederationPlugin({ - name: 'remote', - filename: 'static/chunks/remoteEntry.js', - exposes: { - './Button': './components/Button.js', - }, - remotes: {}, - shared: {}, - extraOptions:{ - exposePages: false - } - }), - ); - - return config; - }, -}; diff --git a/examples/next-mf/remote/package.json b/examples/next-mf/remote/package.json deleted file mode 100644 index a686a112..00000000 --- a/examples/next-mf/remote/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "nextjs-dynamic-ssr_shop", - "version": "0.1.0", - "scripts": { - "dev": "rm -rf .next; NEXT_PRIVATE_LOCAL_WEBPACK=true next dev -p 3002", - "build": "NEXT_PRIVATE_LOCAL_WEBPACK=true next build", - "start": "NEXT_PRIVATE_LOCAL_WEBPACK=true NODE_ENV=production next start -p 3002" - }, - "dependencies": { - "@module-federation/nextjs-mf": "^8.3.5", - "@module-federation/runtime": "^0.1.4", - "lodash": "4.17.21", - "next": "^14.0.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "webpack": "^5.91.0" - } -} \ No newline at end of file diff --git a/examples/next-mf/remote/pages/_app.js b/examples/next-mf/remote/pages/_app.js deleted file mode 100644 index 2a9b1b8f..00000000 --- a/examples/next-mf/remote/pages/_app.js +++ /dev/null @@ -1,8 +0,0 @@ -function MyApp({ Component, pageProps }) { - return ( - <> - - - ); -} -export default MyApp; diff --git a/examples/next-mf/remote/pages/_document.js b/examples/next-mf/remote/pages/_document.js deleted file mode 100644 index 263adca6..00000000 --- a/examples/next-mf/remote/pages/_document.js +++ /dev/null @@ -1,20 +0,0 @@ -import Document, { Html, Head, Main, NextScript } from "next/document"; -import React from "react"; - -class MyDocument extends Document { - render() { - return ( - - - - - -
- - - - ); - } -} - -export default MyDocument; diff --git a/examples/next-mf/remote/pages/index.js b/examples/next-mf/remote/pages/index.js deleted file mode 100644 index bba6db57..00000000 --- a/examples/next-mf/remote/pages/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const Home = (props) => { - return ( -
Hello World
- ) -} - -export default Home; diff --git a/examples/next-mf/remote/public/favicon.ico b/examples/next-mf/remote/public/favicon.ico deleted file mode 100644 index 4965832f2c9b0605eaa189b7c7fb11124d24e48a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeHOOH5Q(7(R0cc?bh2AT>N@1PWL!LLfZKyG5c!MTHoP7_p!sBz0k$?pjS;^lmgJ zU6^i~bWuZYHL)9$wuvEKm~qo~(5=Lvx5&Hv;?X#m}i|`yaGY4gX+&b>tew;gcnRQA1kp zBbm04SRuuE{Hn+&1wk%&g;?wja_Is#1gKoFlI7f`Gt}X*-nsMO30b_J@)EFNhzd1QM zdH&qFb9PVqQOx@clvc#KAu}^GrN`q5oP(8>m4UOcp`k&xwzkTio*p?kI4BPtIwX%B zJN69cGsm=x90<;Wmh-bs>43F}ro$}Of@8)4KHndLiR$nW?*{Rl72JPUqRr3ta6e#A z%DTEbi9N}+xPtd1juj8;(CJt3r9NOgb>KTuK|z7!JB_KsFW3(pBN4oh&M&}Nb$Ee2 z$-arA6a)CdsPj`M#1DS>fqj#KF%0q?w50GN4YbmMZIoF{e1yTR=4ablqXHBB2!`wM z1M1ke9+<);|AI;f=2^F1;G6Wfpql?1d5D4rMr?#f(=hkoH)U`6Gb)#xDLjoKjp)1;Js@2Iy5yk zMXUqj+gyk1i0yLjWS|3sM2-1ECc;MAz<4t0P53%7se$$+5Ex`L5TQO_MMXXi04UDIU+3*7Ez&X|mj9cFYBXqM{M;mw_ zpw>azP*qjMyNSD4hh)XZt$gqf8f?eRSFX8VQ4Y+H3jAtvyTrXr`qHAD6`m;aYmH2zOhJC~_*AuT} zvUxC38|JYN94i(05R)dVKgUQF$}#cxV7xZ4FULqFCNX*Forhgp*yr6;DsIk=ub0Hv zpk2L{9Q&|uI^b<6@i(Y+iSxeO_n**4nRLc`P!3ld5jL=nZRw6;DEJ*1z6Pvg+eW|$lnnjO zjd|8>6l{i~UxI244CGn2kK@cJ|#ecwgSyt&HKA2)z zrOO{op^o*- diff --git a/package.json b/package.json index d39b263f..cc05febe 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "name": "@ethereal-nexus/root", "private": "true", "scripts": { - "postinstall": "turbo run build --filter='./lib/*'" + "postinstall": "turbo run build --filter='./lib/*'", + "build": "turbo run build", + "release": "pnpm --filter \"...{lib/**}\" publish --access public" }, "devDependencies": { "@changesets/cli": "^2.27.8", diff --git a/web/dashboard/src/app/(session)/components/[id]/versions/[versionId]/[tab]/page.tsx b/web/dashboard/src/app/(session)/components/[id]/versions/[versionId]/[tab]/page.tsx index f68e37fa..abbf66cc 100644 --- a/web/dashboard/src/app/(session)/components/[id]/versions/[versionId]/[tab]/page.tsx +++ b/web/dashboard/src/app/(session)/components/[id]/versions/[versionId]/[tab]/page.tsx @@ -1,51 +1,44 @@ import React from 'react'; -import {Separator} from '@/components/ui/separator'; -import {notFound} from 'next/navigation'; -import { - getComponentById, - getComponentDependentsProjectsWithOwners, - getComponentVersions -} from "@/data/components/actions"; -import ComponentVersionHeader from "@/components/components/component/version/header"; -import ComponentVersionTabs from "@/components/components/component/version/tabs"; -import {auth} from "@/auth"; -import {getMembersByResourceId} from "@/data/member/actions"; -import {getResourceEvents} from "@/data/events/actions"; +import { Separator } from '@/components/ui/separator'; +import { notFound } from 'next/navigation'; +import { getComponentById, getComponentDependentsProjects, getComponentVersions } from '@/data/components/actions'; +import ComponentVersionHeader from '@/components/components/component/version/header'; +import ComponentVersionTabs from '@/components/components/component/version/tabs'; +import { auth } from '@/auth'; +import { getResourceEvents } from '@/data/events/actions'; export default async function EditComponentVersion({params: {id, versionId, tab}}: any) { const session = await auth() const component = await getComponentById(id); const versions = await getComponentVersions(id); - const dependents = await getComponentDependentsProjectsWithOwners(id); + const dependents = await getComponentDependentsProjects(id, session?.user?.id); const events = await getResourceEvents(id); - const projects = await getComponentDependentsProjectsWithOwners(id); - if (projects.success) { - projects.data = await Promise.all( - projects.data.map(async (project) => { - const membersData = await getMembersByResourceId(project.id, session?.user?.id); - const userHasAccess = (membersData.success && membersData.data.some(member => member.user_id == session?.user?.id)); - - return { - ...project, - userHasAccess - }; - }) - ); - } - - if (!versions.success || !component.success || !dependents.success || !projects.success) { + if (!versions.success || !component.success || !dependents.success ) { notFound(); } + + const safeDependents = dependents.data.map(p => p.has_access ? p : null) const selectedVersion = versions.data.filter((version: any) => version.id === versionId)[0]; + return (
- + - +
); } diff --git a/web/dashboard/src/app/(session)/projects/[id]/page.tsx b/web/dashboard/src/app/(session)/projects/[id]/page.tsx index 2e710a0a..89bbd998 100644 --- a/web/dashboard/src/app/(session)/projects/[id]/page.tsx +++ b/web/dashboard/src/app/(session)/projects/[id]/page.tsx @@ -9,8 +9,9 @@ import Link from 'next/link'; import ProjectsForm from '@/components/projects/project-form'; import { getResourceEvents } from '@/data/events/actions'; import { ProjectEvents } from '@/components/projects/project-events/project-events'; +import { EnvironmentsList } from '@/components/projects/environments-table/environment-list'; -export default async function EditProject({ params: { id }, searchParams: { tab } }: any) { +export default async function EditProject({ params: { id }, searchParams: { tab, env } }: any) { const session = await auth(); const project = await getProjectById(id, session?.user?.id); const events = await getResourceEvents(id); @@ -39,6 +40,11 @@ export default async function EditProject({ params: { id }, searchParams: { tab Users + + + Environments + + Settings @@ -52,7 +58,9 @@ export default async function EditProject({ params: { id }, searchParams: { tab @@ -60,13 +68,18 @@ export default async function EditProject({ params: { id }, searchParams: { tab id={id} /> + + + - + diff --git a/web/dashboard/src/app/(session)/projects/launch/new/[id]/page.tsx b/web/dashboard/src/app/(session)/projects/launch/new/[id]/page.tsx new file mode 100644 index 00000000..3a3f9a45 --- /dev/null +++ b/web/dashboard/src/app/(session)/projects/launch/new/[id]/page.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { ComparisonResult, LaunchesList } from '@/components/launches/table/launches-list'; +import { getEnvironmentsById, getEnvironmentsByProject } from '@/data/projects/actions'; +import { auth } from '@/auth'; +import { EnvironmentWithComponents } from '@/data/projects/dto'; +import { notFound } from 'next/navigation'; +import { EnvironmentPicker } from './picker'; + +function compareEnvironments(from: EnvironmentWithComponents, to: EnvironmentWithComponents): ComparisonResult[] { + const result: ComparisonResult[] = []; + + from.components.forEach((compFrom) => { + const compTo = to.components.find((c) => c.id === compFrom.id); + + if (compTo) { + result.push({ + id: compFrom.id, + name: compFrom.name, + title: compFrom.title, + new: false, + is_active: { + from: compFrom.is_active, + to: compTo.is_active, + }, + version: { + from: compFrom.version || 'latest', + to: compTo.version || 'latest', + }, + }); + } else { + result.push({ + id: compFrom.id, + name: compFrom.name, + title: compFrom.title, + new: true, + is_active: { + from: compFrom.is_active, + to: null, + }, + version: { + from: compFrom.version || 'latest', + to: null, + }, + }) + } + }); + + return result; +} + +export default async function NewLaunch({ params: { id } }: any) { + const session = await auth() + const [fromId, toId] = id.split('...') + + const from = await getEnvironmentsById(fromId, session?.user?.id) + const to = await getEnvironmentsById(toId, session?.user?.id) + + if (!from.success || !to.success || from.data.project_id !== to.data.project_id) { + notFound(); + } + + const environments = await getEnvironmentsByProject(from.data.project_id, session?.user?.id) + const comparison = compareEnvironments(from.data, to.data); + + return ( +
+
+
+
+

Create Launch

+
+

Compare the changes to launch

+
+
+ + +
+ ); +} diff --git a/web/dashboard/src/app/(session)/projects/launch/new/[id]/picker.tsx b/web/dashboard/src/app/(session)/projects/launch/new/[id]/picker.tsx new file mode 100644 index 00000000..a8a9c9b8 --- /dev/null +++ b/web/dashboard/src/app/(session)/projects/launch/new/[id]/picker.tsx @@ -0,0 +1,103 @@ +'use client'; + +import React, { useState } from 'react'; +import { Check, ChevronDownIcon, Combine } from 'lucide-react'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; +import { Separator } from '@/components/ui/separator'; +import Link from 'next/link'; + +export function EnvironmentPicker({ from, to, environments }) { + const [isFromOpen, setIsFromOpen] = useState(false); + const [isToOpen, setIsToOpen] = useState(false); + + return
+ +
+ + + + + + + + + + {environments + .map(env => ( + + + + {from.id === env.id ? + : + null + } +

{env.name}

+
+
+ + ) + ) + } +
+
+
+
+
+ ... + + + + + + + + + + {environments + .map(env => ( + + + + {to.id === env.id ? + : + null + } +

{env.name}

+
+
+ + ) + ) + } +
+
+
+
+
+
+
; +} \ No newline at end of file diff --git a/web/dashboard/src/app/api/v1/environments/[id]/components/[name]/route.ts b/web/dashboard/src/app/api/v1/environments/[id]/components/[name]/route.ts new file mode 100644 index 00000000..68b321bf --- /dev/null +++ b/web/dashboard/src/app/api/v1/environments/[id]/components/[name]/route.ts @@ -0,0 +1,92 @@ +import { HttpStatus } from '@/app/api/utils'; +import { authenticatedWithKey } from '@/lib/route-wrappers'; +import { NextResponse } from 'next/server'; +import { getEnvironmentComponentConfig, getProjectComponentConfig } from '@/data/projects/actions'; + +/** + * @swagger + * /api/v1/projects/{id}/components/{name}: + * get: + * summary: Get a component by name and project + * description: Get a component by name and assigned project, listing all its versions + * tags: + * - Projects + * produces: + * - application/json + * parameters: + * - in: path + * name: id + * description: The project's id + * required: true + * type: string + * - in: path + * name: name + * description: The component's name + * required: true + * type: string + * responses: + * '200': + * description: The component info + * content: + * application/json: + * schema: + * type: object + * $ref: '#/components/schemas/Component' + * '404': + * description: Not Found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Not Found - the component does not exist + * '500': + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Internal Server Error - Something went wrong on the server side + */ +export const GET = authenticatedWithKey( + async ( + _, + ext: { params: { id: string; name: string }; user } | undefined, + ) => { + const { id, name } = ext?.params || { id: undefined, name: undefined }; + if (!id) { + return NextResponse.json('No identifier provided.', { + status: HttpStatus.BAD_REQUEST, + }); + } + + const userId = ext?.user.id; + if (!userId) { + return NextResponse.json('Api key not provided or invalid.', { + status: HttpStatus.BAD_REQUEST, + }); + } + + const permissions = ext?.user.permissions; + if (permissions[id] === 'none') { + return NextResponse.json('You do not have permissions for this resource.', { + status: HttpStatus.FORBIDDEN, + }); + } + + const response = await getEnvironmentComponentConfig(id, name, userId); + + if (!response.success) { + return NextResponse.json(response.error, { + status: HttpStatus.BAD_REQUEST, + }); + } + + return NextResponse.json(response.data, { status: HttpStatus.OK }); + }, +); diff --git a/web/dashboard/src/app/api/v1/environments/[id]/components/[name]/ssr/route.ts b/web/dashboard/src/app/api/v1/environments/[id]/components/[name]/ssr/route.ts new file mode 100644 index 00000000..8e0395e6 --- /dev/null +++ b/web/dashboard/src/app/api/v1/environments/[id]/components/[name]/ssr/route.ts @@ -0,0 +1,112 @@ +import {HttpStatus} from '@/app/api/utils'; +import {authenticatedWithKey} from '@/lib/route-wrappers'; +import {NextResponse} from 'next/server'; +import { getEnvironmentComponentConfig, getProjectComponentConfig } from '@/data/projects/actions'; +import {callSSR} from "@/lib/ssr/ssr"; +import crypto from 'crypto'; +import { LRUCache } from '@/lib/cache/LRUCache'; + +const cache = new LRUCache(100); // Set the cache capacity to 100 + +/** + * @swagger + * /api/v1/projects/{id}/components/{name}/ssr: + * post: + * summary: Returns component SSR output + * description: Returns component SSR output based on the props sent as json body + * tags: + * - Projects + * produces: + * - application/json + * parameters: + * - in: path + * name: id + * description: The project's id + * required: true + * type: string + * - in: path + * name: name + * description: The component's name + * required: true + * type: string + * responses: + * '200': + * description: The SSR component + * content: + * application/json: + * schema: + * type: object + * $ref: '#/components/schemas/Component' + * '404': + * description: Not Found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Not Found - the component does not exist + * '500': + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Internal Server Error - Something went wrong on the server side + */ +export const POST = authenticatedWithKey( + async ( + request, + ext: { params: { id: string; name: string }; user } | undefined, + ) => { + const {id, name} = ext?.params || {id: undefined, name: undefined}; + if (!id) { + return NextResponse.json('No identifier provided.', { + status: HttpStatus.BAD_REQUEST, + }); + } + + const userId = ext?.user.id; + if (!userId) { + return NextResponse.json('Api key not provided or invalid.', { + status: HttpStatus.BAD_REQUEST, + }); + } + + const permissions = ext?.user.permissions; + if (permissions[id] === 'none') { + return NextResponse.json('You do not have permissions for this resource.', { + status: HttpStatus.FORBIDDEN, + }); + } + + const req = await request.json(); + const response = await getEnvironmentComponentConfig(id, name, userId); + + const reqHash = crypto.createHash('sha256').update(JSON.stringify(req) + JSON.stringify(response)).digest('hex'); + + + if (cache.get(reqHash)) { + return NextResponse.json(cache.get(reqHash), { status: HttpStatus.OK }); + } + + if (!response.success) { + return NextResponse.json(response.error, { + status: HttpStatus.BAD_REQUEST, + }); + } + const {output, serverSideProps } = await callSSR(response.data.name, req, response.data.assets); + + if (output !== "") { + const result = {output, serverSideProps}; + cache.set(reqHash, result); + return NextResponse.json(result, { status: HttpStatus.OK }); + } + + return NextResponse.json({output, serverSideProps}, { status: HttpStatus.OK }); + }, +); diff --git a/web/dashboard/src/app/api/v1/environments/[id]/components/route.ts b/web/dashboard/src/app/api/v1/environments/[id]/components/route.ts new file mode 100644 index 00000000..c1872e4d --- /dev/null +++ b/web/dashboard/src/app/api/v1/environments/[id]/components/route.ts @@ -0,0 +1,84 @@ +import { HttpStatus } from '@/app/api/utils'; +import { getActiveEnvironmentComponents, getActiveProjectComponents } from '@/data/projects/actions'; +import { authenticatedWithKey } from '@/lib/route-wrappers'; +import { NextResponse } from 'next/server'; + +/** + * @swagger + * /api/v1/projects/{id}/components: + * get: + * summary: Get a project's components + * description: Get the components linked to a project + * tags: + * - Projects + * produces: + * - application/json + * parameters: + * - in: path + * name: id + * description: The project's ID + * required: true + * type: string + * responses: + * '200': + * description: The project info + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Component' + * '404': + * description: Not Found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Not Found - the project does not exist + * '500': + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Internal Server Error - Something went wrong on the server side + */ +export const GET = authenticatedWithKey( + async (_, ext: { params: { id: string }, user } | undefined) => { + const { id } = ext?.params || { id: undefined }; + if (!id) { + return NextResponse.json('No identifier provided.', { + status: HttpStatus.BAD_REQUEST, + }); + } + + const userId = ext?.user.id; + if (!userId) { + return NextResponse.json('Api key not provided or invalid.', { + status: HttpStatus.BAD_REQUEST, + }); + } + + const permissions = ext?.user.permissions; + if (permissions[id] === 'none') { + return NextResponse.json('You do not have permissions for this resource.', { + status: HttpStatus.FORBIDDEN, + }); + } + + const response = await getActiveEnvironmentComponents(id, userId); + if (!response.success) { + return NextResponse.json(response.error, { + status: HttpStatus.BAD_REQUEST, + }); + } + + return NextResponse.json(response.data, { status: HttpStatus.OK }); + }, +); diff --git a/web/dashboard/src/components/components/component/version/tabs.tsx b/web/dashboard/src/components/components/component/version/tabs.tsx index 13e75eeb..9f200358 100644 --- a/web/dashboard/src/components/components/component/version/tabs.tsx +++ b/web/dashboard/src/components/components/component/version/tabs.tsx @@ -7,9 +7,7 @@ import Readme from "@/components/components/component/version/tabs/readme"; import Dependents from "@/components/components/component/version/tabs/dependents"; import Events from "@/components/components/component/version/tabs/events"; - - -const ComponentVersionTabs = ({activeTab, component, versions, selectedVersion,dependents,events}) => { +const ComponentVersionTabs = ({activeTab, component, versions, selectedVersion, dependents, events}) => { const {push} = useRouter(); return ( @@ -30,7 +28,7 @@ const ComponentVersionTabs = ({activeTab, component, versions, selectedVersion,d selectedVersionId={selectedVersion.id}/> - +
    diff --git a/web/dashboard/src/components/components/component/version/tabs/dependents.tsx b/web/dashboard/src/components/components/component/version/tabs/dependents.tsx index da4cfabf..8cb1fadf 100644 --- a/web/dashboard/src/components/components/component/version/tabs/dependents.tsx +++ b/web/dashboard/src/components/components/component/version/tabs/dependents.tsx @@ -1,74 +1,78 @@ -'use client' +'use client'; import React from 'react'; -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"; -import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"; -import {Button} from "@/components/ui/button"; -import {MoreHorizontalIcon} from "lucide-react"; -import {HomeIcon, PersonIcon} from "@radix-ui/react-icons"; -import {ProjectWithOwners} from "@/data/projects/dto"; -import Link from "next/link"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { MoreHorizontalIcon } from 'lucide-react'; +import { HomeIcon, PersonIcon } from '@radix-ui/react-icons'; +import { ProjectWithOwners } from '@/data/projects/dto'; +import Link from 'next/link'; interface PreviewProps { - dependents: DependentsProps[]; + dependents: (ProjectWithOwners | null)[]; } -interface DependentsProps extends ProjectWithOwners{ - userHasAccess: boolean; -} - -const Dependents: React.FC = ({dependents = []}) => { - const dependentsWithAccess = dependents.filter((dep) => dep.userHasAccess) - const totalDependents = dependents.length - dependentsWithAccess.length; +const Dependents: React.FC = ({ dependents = [] }) => { + const hiddenDependents = dependents.filter(p => p === null); + const accessibleDependents = dependents.filter(p => p !== null); - return ( -
    - {dependents.length === 0 && ( - No dependent components - )} - {dependentsWithAccess.length === 0 ? No access to the dependent components : - dependentsWithAccess.map((dependent) => ( - - - - -
    - {dependent.name} - {dependent.description} -
    - - - - - - View Project - - -
    - -
    -
    Project Maintainers:
    -
    - {dependent.owners.map((owner, index) => ( -
    - - {owner} -
    - ))} -
    -
    -
    -
    - - )) - } - {totalDependents > 0 && ( - {totalDependents} more other dependent components - )} -
    - ); + return ( +
    + {dependents.length === 0 && ( + No dependent projects. + )} + {accessibleDependents.length === 0 ? + No access to the dependent projects. : + accessibleDependents + .map((dependent) => ( + + + + +
    + {dependent.name} + {dependent.description} +
    + + + + + + View Project + + +
    + +
    +
    Project Maintainers:
    +
    + {dependent.owners.map(({ name }, index) => ( +
    + + {name} +
    + ))} +
    +
    +
    +
    + + )) + } + {hiddenDependents.length > 0 && ( + {hiddenDependents.length} other dependent projects. + )} +
    + ); }; export default Dependents; diff --git a/web/dashboard/src/components/launches/table/columns.tsx b/web/dashboard/src/components/launches/table/columns.tsx new file mode 100644 index 00000000..822edc54 --- /dev/null +++ b/web/dashboard/src/components/launches/table/columns.tsx @@ -0,0 +1,81 @@ +'use client'; + +import * as React from 'react'; +import { DataTableColumnHeader } from '@/components/ui/data-table/data-table-column-header'; +import { Badge } from '@/components/ui/badge'; +import { ArrowRight } from 'lucide-react'; + +export const columns = [ + { + id: 'name', + accessorFn: row => row.name, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + row.original.new ? + New : + null + }{row.getValue('name')} + , + enableSorting: true, + enableHiding: true + }, + { + id: 'title', + accessorFn: row => row.title, + header: ({ column }) => ( + + ), + cell: ({ row }) => {row.getValue('title')}, + enableSorting: true, + enableHiding: true + }, + { + id: 'version', + accessorFn: row => row.version, + header: ({ column }) => ( + + ), + cell: ({ row }) =>
    + {row.original.new ? + {row.getValue('version').from} : + row.original.version.to === row.original.version.from ? + {row.original.version.from} : + <> + {row.getValue('version').to} + + {row.getValue('version').from} + + } +
    , + enableSorting: false, + enableHiding: true + }, + { + id: 'active', + accessorFn: row => row.is_active, + header: ({ column }) => ( + + ), + cell: + ({ row }) =>
    + {row.original.new ? + {row.getValue('active').from ? 'Active' : 'Inactive'} : + row.original.is_active.to === row.original.is_active.from ? + {row.original.is_active.to ? 'Active' : 'Inactive'} : + <> + {row.getValue('active').to ? 'Active' : 'Inactive'} + + {row.getValue('active').from ? 'Active' : 'Inactive'} + + } +
    , + enableSorting: false, + enableHiding: true + } +]; + + diff --git a/web/dashboard/src/components/launches/table/launch-button.tsx b/web/dashboard/src/components/launches/table/launch-button.tsx new file mode 100644 index 00000000..a1eaa22e --- /dev/null +++ b/web/dashboard/src/components/launches/table/launch-button.tsx @@ -0,0 +1,75 @@ +'use client'; + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Rocket } from 'lucide-react'; +import { Dialog, DialogTrigger } from '@radix-ui/react-dialog'; +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { toast } from '@/components/ui/use-toast'; +import { Environment } from '@/data/projects/dto'; +import { launch } from '@/data/launches/actions'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; + +export type LaunchButtonProps = { + from: Pick, + to: Pick, +} + +export function LaunchButton({ from, to }: LaunchButtonProps) { + const { data: session } = useSession(); + const { replace } = useRouter(); + + const [isLaunchDialogOpen, setIsLaunchDialogOpen] = useState(false); + + const handleLaunch = async () => { + const result = await launch(from.id, to.id, session?.user?.id); + if (!result.success) { + toast({ + title: `Launch from ${from.name} to ${to.name} could not be completed.` + }); + } + + toast({ + title: `Launch from ${from.name} to ${to.name} was successfull!.` + }); + setIsLaunchDialogOpen(false); + replace(`/projects/${to.project_id}?env=${to.id}`); + }; + + return + + + + + + Are you sure? + + You will now merge the configs from dev into main. Are you sure you want to continue? + + + + + + + + ; +} diff --git a/web/dashboard/src/components/launches/table/launches-list.tsx b/web/dashboard/src/components/launches/table/launches-list.tsx new file mode 100644 index 00000000..f82a7c81 --- /dev/null +++ b/web/dashboard/src/components/launches/table/launches-list.tsx @@ -0,0 +1,32 @@ +import { DataTable } from '@/components/ui/data-table/data-table'; +import { columns } from './columns'; +import React from 'react'; +import { Component } from '@/data/components/dto'; +import { LaunchButton, LaunchButtonProps } from '@/components/launches/table/launch-button'; + +export type ComparisonResult = Pick & { + new: boolean; + is_active: { + from: boolean | null; + to: boolean | null; + }; + version: { + from: string | null; + to: string | null; + }; +}; + +type LaunchesListProps = { + comparison: ComparisonResult[]; +} & LaunchButtonProps + +export async function LaunchesList({comparison, ...launchProps}: LaunchesListProps) { + return + } + />; +} diff --git a/web/dashboard/src/components/projects/component-selection-table/actions.tsx b/web/dashboard/src/components/projects/component-selection-table/actions.tsx index 77e316a0..3b1b70b2 100644 --- a/web/dashboard/src/components/projects/component-selection-table/actions.tsx +++ b/web/dashboard/src/components/projects/component-selection-table/actions.tsx @@ -20,13 +20,14 @@ export function ProjectsComponentsRowActions({ table, row }) { const component = row.original; const projectId = table.options.meta.projectId const {data: session} = useSession() + const isDisabled = session?.permissions[projectId] !== 'write'; const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const handleDeleteOk = async () => { setDeleteDialogOpen(false) if (component) { - const deleted = await deleteComponentConfig(component.config_id, session?.user?.id) + const deleted = await deleteComponentConfig(component.config_id, projectId, session?.user?.id) if(deleted.success) { toast({ @@ -80,6 +81,7 @@ export function ProjectsComponentsRowActions({ table, row }) { Cancel + + + + + + + { + environments + .map(env => ( + + + { + environment === env.id ? + : + null + } +

    {env.name}

    +
    +
    + ) + ) + } +
    +
    +
    +
    + + + + + + + + + + + {environments + .filter(env => env.id !== environment) + .map(env => ( + + + {environment === env.id ? + : + null + } + {selected?.name}...{env.name} + + + ) + ) + } + + + + + + +