From ba93732222111355f0f42a46d609600ca17591e0 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Wed, 30 Apr 2025 22:58:12 +1200 Subject: [PATCH 1/4] move team engines to projects --- .../analytics/analytics-page.tsx | 0 .../{ => (overview)}/analytics/filter.tsx | 4 +- .../analytics/ftux.client.tsx | 2 +- .../analytics/send-test-tx.client.tsx | 0 .../{ => (overview)}/analytics/summary.tsx | 0 .../analytics/tx-chart/tx-chart-ui.tsx | 0 .../analytics/tx-chart/tx-chart.tsx | 0 .../analytics/tx-table/tx-table-ui.tsx | 2 +- .../analytics/tx-table/tx-table.tsx | 0 .../analytics/tx-table/types.ts | 0 .../engine/{ => (overview)}/explorer/page.tsx | 0 .../engine/{ => (overview)}/layout.tsx | 37 +- .../engine/{ => (overview)}/lib/analytics.ts | 4 +- .../engine/{ => (overview)}/lib/utils.ts | 0 .../{ => (overview)}/lib/vault.client.ts | 2 +- .../engine/{ => (overview)}/page.tsx | 8 +- .../create-server-wallet.client.tsx | 150 ++ .../server-wallets/components/try-it-out.tsx | 303 +++ .../{ => (overview)}/server-wallets/page.tsx | 4 +- .../server-wallets/wallet-table/types.ts | 0 .../wallet-table/wallet-table-ui.client.tsx | 8 +- .../wallet-table/wallet-table.tsx | 0 .../{ => (overview)}/tx/[id]/layout.tsx | 0 .../engine/{ => (overview)}/tx/[id]/page.tsx | 4 +- .../create-vault-account.client.tsx | 0 .../vault/components/key-management.tsx | 0 .../components/list-access-tokens.client.tsx | 0 .../components/rotate-admin-key.client.tsx | 322 +++ .../engine/{ => (overview)}/vault/page.tsx | 2 +- .../(general)/EngineFooterCard.stories.tsx | 44 + .../engine/legacy/(general)/_components.tsx | 167 ++ .../import/EngineImportPage.stories.tsx | 37 + .../(general)/import/EngineImportPage.tsx | 193 ++ .../engine/legacy/(general)/import/page.tsx | 27 + .../engine/legacy/(general)/layout.tsx | 113 ++ .../engine-instances-table.stories.tsx | 166 ++ .../overview/engine-instances-table.tsx | 938 +++++++++ .../legacy/(general)/overview/engine-list.tsx | 26 + .../engine/legacy/(general)/page.tsx | 52 + .../_components/EngineErrorPage.tsx | 28 + .../_components/EnginePageLayout.tsx | 72 + .../_components/EnsureEnginePermission.tsx | 87 + .../[engineId]/_components/version.tsx | 289 +++ .../components/access-tokens-table.tsx | 312 +++ .../components/add-access-token-button.tsx | 123 ++ .../components/add-keypair-button.tsx | 233 +++ .../components/engine-access-tokens.tsx | 205 ++ .../components/keypairs-table.tsx | 218 ++ .../[engineId]/access-tokens/page.tsx | 15 + .../admins/components/add-admin-button.tsx | 131 ++ .../admins/components/admins-table.tsx | 300 +++ .../admins/components/engine-admins.tsx | 48 + .../(instance)/[engineId]/admins/page.tsx | 13 + .../components/EngineAlertDialogForm.tsx | 231 +++ .../alerts/components/EngineAlertsPage.tsx | 37 + .../components/EngineDeleteAlertModal.tsx | 52 + .../components/ManageEngineAlerts.stories.tsx | 122 ++ .../alerts/components/ManageEngineAlerts.tsx | 387 ++++ .../components/RecentEngineAlerts.stories.tsx | 65 + .../alerts/components/RecentEngineAlerts.tsx | 144 ++ .../(instance)/[engineId]/alerts/page.tsx | 15 + .../components/circle-config.tsx | 129 ++ .../configuration/components/cors.tsx | 105 + .../components/engine-configuration.tsx | 35 + .../components/engine-wallet-config.tsx | 117 ++ .../configuration/components/ip-allowlist.tsx | 112 ++ .../components/kms-aws-config.tsx | 185 ++ .../components/kms-gcp-config.tsx | 232 +++ .../configuration/components/local-config.tsx | 10 + .../configuration/components/system.tsx | 33 + .../[engineId]/configuration/page.tsx | 19 + .../add-contract-subscription-button.tsx | 566 ++++++ .../contract-subscriptions-table.tsx | 470 +++++ .../engine-contract-subscription.tsx | 73 + .../contract-subscriptions/page.tsx | 18 + .../explorer/components/engine-explorer.tsx | 38 + .../explorer/components/swagger-ui.css | 1759 +++++++++++++++++ .../(instance)/[engineId]/explorer/page.tsx | 13 + .../legacy/(instance)/[engineId]/layout.tsx | 134 ++ .../components/EngineSystemMetrics.tsx | 149 ++ .../metrics/components/ErrorRate.tsx | 110 ++ .../metrics/components/Healthcheck.tsx | 47 + .../metrics/components/StatusCodes.tsx | 152 ++ .../(instance)/[engineId]/metrics/page.tsx | 19 + .../components/backend-wallets-table.tsx | 732 +++++++ .../create-backend-wallet-button.tsx | 333 ++++ .../overview/components/engine-overview.tsx | 151 ++ .../import-backend-wallet-button.tsx | 347 ++++ .../components/transaction-timeline.tsx | 295 +++ .../components/transactions-table.tsx | 878 ++++++++ .../[engineId]/overview/components/utils.ts | 11 + .../legacy/(instance)/[engineId]/page.tsx | 19 + .../components/add-relayer-button.tsx | 239 +++ .../relayers/components/engine-relayer.tsx | 50 + .../relayers/components/relayers-table.tsx | 449 +++++ .../(instance)/[engineId]/relayers/page.tsx | 13 + .../legacy/(instance)/[engineId]/types.ts | 6 + .../create-wallet-credential-button.tsx | 78 + .../components/credential-form.tsx | 171 ++ .../credential-type-fields/circle.tsx | 62 + .../edit-wallet-credential-button.tsx | 97 + .../wallet-credentials/components/types.ts | 23 + .../components/wallet-credentials-table.tsx | 105 + .../components/wallet-credentials.tsx | 93 + .../[engineId]/wallet-credentials/page.tsx | 18 + .../components/add-webhook-button.tsx | 146 ++ .../webhooks/components/engine-webhooks.tsx | 49 + .../webhooks/components/webhooks-table.tsx | 332 ++++ .../(instance)/[engineId]/webhooks/page.tsx | 13 + .../_utils/getEngineAccessPermission.ts | 33 + .../engine/legacy/_utils/getEngineInstance.ts | 46 + .../_utils/getEngineInstancePageMeta.ts | 35 + .../legacy/_utils/getEngineInstances.ts | 33 + .../create-server-wallet.client.tsx | 2 +- .../components/rotate-admin-key.client.tsx | 242 +++ .../server-wallets/components/try-it-out.tsx | 6 +- .../components/rotate-admin-key.client.tsx | 2 +- .../TeamAndProjectSelectorPopoverButton.tsx | 7 +- 118 files changed, 15348 insertions(+), 35 deletions(-) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/analytics-page.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/filter.tsx (87%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/ftux.client.tsx (97%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/send-test-tx.client.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/summary.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/tx-chart/tx-chart-ui.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/tx-chart/tx-chart.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/tx-table/tx-table-ui.tsx (99%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/tx-table/tx-table.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/analytics/tx-table/types.ts (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/explorer/page.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/layout.tsx (70%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/lib/analytics.ts (97%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/lib/utils.ts (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/lib/vault.client.ts (98%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/page.tsx (94%) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/create-server-wallet.client.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/try-it-out.tsx rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/server-wallets/page.tsx (92%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/server-wallets/wallet-table/types.ts (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/server-wallets/wallet-table/wallet-table-ui.client.tsx (94%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/server-wallets/wallet-table/wallet-table.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/tx/[id]/layout.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/tx/[id]/page.tsx (84%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/vault/components/create-vault-account.client.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/vault/components/key-management.tsx (100%) rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/vault/components/list-access-tokens.client.tsx (100%) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/rotate-admin-key.client.tsx rename apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/{ => (overview)}/vault/page.tsx (92%) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/EngineFooterCard.stories.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/_components.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.stories.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/layout.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.stories.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-list.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/_components/EngineErrorPage.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/_components/EnginePageLayout.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/_components/EnsureEnginePermission.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/_components/version.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/access-tokens/components/access-tokens-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/access-tokens/components/add-access-token-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/access-tokens/components/add-keypair-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/access-tokens/components/engine-access-tokens.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/access-tokens/components/keypairs-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/access-tokens/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/admins/components/add-admin-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/admins/components/admins-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/admins/components/engine-admins.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/admins/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/components/EngineAlertDialogForm.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/components/EngineAlertsPage.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/components/EngineDeleteAlertModal.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/components/ManageEngineAlerts.stories.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/components/ManageEngineAlerts.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/components/RecentEngineAlerts.stories.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/components/RecentEngineAlerts.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/alerts/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/circle-config.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/cors.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/engine-configuration.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/engine-wallet-config.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/ip-allowlist.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/kms-aws-config.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/kms-gcp-config.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/local-config.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/components/system.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/configuration/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/contract-subscriptions/components/add-contract-subscription-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/contract-subscriptions/components/contract-subscriptions-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/contract-subscriptions/components/engine-contract-subscription.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/contract-subscriptions/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/explorer/components/engine-explorer.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/explorer/components/swagger-ui.css create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/explorer/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/layout.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/metrics/components/EngineSystemMetrics.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/metrics/components/ErrorRate.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/metrics/components/Healthcheck.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/metrics/components/StatusCodes.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/metrics/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/overview/components/backend-wallets-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/overview/components/create-backend-wallet-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/overview/components/engine-overview.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/overview/components/import-backend-wallet-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/overview/components/transaction-timeline.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/overview/components/transactions-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/overview/components/utils.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/relayers/components/add-relayer-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/relayers/components/engine-relayer.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/relayers/components/relayers-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/relayers/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/types.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/components/create-wallet-credential-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/components/credential-form.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/components/credential-type-fields/circle.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/components/edit-wallet-credential-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/components/types.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/components/wallet-credentials-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/components/wallet-credentials.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/wallet-credentials/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/webhooks/components/add-webhook-button.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/webhooks/components/engine-webhooks.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/webhooks/components/webhooks-table.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(instance)/[engineId]/webhooks/page.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/_utils/getEngineAccessPermission.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/_utils/getEngineInstance.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/_utils/getEngineInstancePageMeta.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/_utils/getEngineInstances.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/components/rotate-admin-key.client.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/analytics-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/analytics-page.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/analytics-page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/analytics-page.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/filter.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/filter.tsx similarity index 87% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/filter.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/filter.tsx index 8679439c57d..773264f8375 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/filter.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/filter.tsx @@ -5,8 +5,8 @@ import { useResponsiveSearchParams, useSetResponsiveSearchParams, } from "responsive-rsc"; -import { DateRangeSelector } from "../../../../../../../components/analytics/date-range-selector"; -import { IntervalSelector } from "../../../../../../../components/analytics/interval-selector"; +import { DateRangeSelector } from "../../../../../../../../components/analytics/date-range-selector"; +import { IntervalSelector } from "../../../../../../../../components/analytics/interval-selector"; import { getTxAnalyticsFiltersFromSearchParams } from "../lib/utils"; export function TransactionAnalyticsFilter() { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/ftux.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/ftux.client.tsx similarity index 97% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/ftux.client.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/ftux.client.tsx index 49c4ef9f131..cb7e6eb2950 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/ftux.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/ftux.client.tsx @@ -3,7 +3,7 @@ import type { Project } from "@/api/projects"; import { type Step, StepsCard } from "components/dashboard/StepsCard"; import Link from "next/link"; import { useMemo, useState } from "react"; -import { Button } from "../../../../../../../@/components/ui/button"; +import { Button } from "../../../../../../../../@/components/ui/button"; import CreateServerWallet from "../server-wallets/components/create-server-wallet.client"; import type { Wallet } from "../server-wallets/wallet-table/types"; import CreateVaultAccountButton from "../vault/components/create-vault-account.client"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/send-test-tx.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/send-test-tx.client.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/send-test-tx.client.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/send-test-tx.client.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/summary.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/summary.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/summary.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/summary.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-chart/tx-chart-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-chart/tx-chart-ui.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-chart/tx-chart-ui.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-chart/tx-chart-ui.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-chart/tx-chart.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-chart/tx-chart.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-chart/tx-chart.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-chart/tx-chart.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-table/tx-table-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-table/tx-table-ui.tsx similarity index 99% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-table/tx-table-ui.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-table/tx-table-ui.tsx index 117ee68f629..cd8daf51e23 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-table/tx-table-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-table/tx-table-ui.tsx @@ -37,7 +37,7 @@ import { useAllChainsData } from "hooks/chains/allChains"; import { ExternalLinkIcon, InfoIcon } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; -import { ChainIconClient } from "../../../../../../../../components/icons/ChainIcon"; +import { ChainIconClient } from "../../../../../../../../../components/icons/ChainIcon"; import type { Wallet } from "../../server-wallets/wallet-table/types"; import type { Transaction, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-table/tx-table.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-table/tx-table.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-table/tx-table.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-table/tx-table.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-table/types.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-table/types.ts similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/analytics/tx-table/types.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/analytics/tx-table/types.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/explorer/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/explorer/page.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/explorer/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/explorer/page.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/layout.tsx similarity index 70% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/layout.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/layout.tsx index eea3d9a80dd..35d14daf36f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/layout.tsx @@ -1,22 +1,30 @@ import { getProject } from "@/api/projects"; import { getTeamBySlug } from "@/api/team"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { TabPathLinks } from "@/components/ui/tabs"; import { THIRDWEB_ENGINE_CLOUD_URL } from "@/constants/env"; -import { CloudIcon } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; -import { Button } from "../../../../../../@/components/ui/button"; +import { EngineIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon"; +import { getAuthToken } from "../../../../../api/lib/getAuthToken"; +import { getEngineInstances } from "../legacy/_utils/getEngineInstances"; export default async function Page(props: { params: Promise<{ team_slug: string; project_slug: string }>; children: React.ReactNode; }) { const { team_slug, project_slug } = await props.params; + const authToken = await getAuthToken(); - const [team, project] = await Promise.all([ + if (!authToken) { + redirect("/team"); + } + + const [team, project, engineInstances] = await Promise.all([ getTeamBySlug(team_slug), getProject(team_slug, project_slug), + getEngineInstances({ authToken, teamIdOrSlug: team_slug }), ]); if (!team) { @@ -27,6 +35,11 @@ export default async function Page(props: { redirect(`/team/${team_slug}`); } + // if we have any legacy engine instances, redirect to the legacy engine layout + if (engineInstances.data?.length && engineInstances.data.length > 0) { + redirect(`/team/${team_slug}/${project_slug}/engine/legacy`); + } + return (
-

- Engine +

+ Engine{" "} + + Cloud +

{THIRDWEB_ENGINE_CLOUD_URL} - - - Cloud -
- - + +
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/analytics.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/analytics.ts similarity index 97% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/analytics.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/analytics.ts index df00470dc4c..05020cfeda5 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/analytics.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/analytics.ts @@ -1,6 +1,6 @@ import { THIRDWEB_ENGINE_CLOUD_URL } from "@/constants/env"; -import type { TransactionStats } from "../../../../../../../types/analytics"; -import { getAuthToken } from "../../../../../api/lib/getAuthToken"; +import type { TransactionStats } from "../../../../../../../../types/analytics"; +import { getAuthToken } from "../../../../../../api/lib/getAuthToken"; // Define the structure of the data we expect back from our fetch function export type TransactionSummaryData = { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/utils.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/utils.ts similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/utils.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/utils.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/vault.client.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/vault.client.ts similarity index 98% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/vault.client.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/vault.client.ts index f66f308ab93..225f87bed3c 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/lib/vault.client.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/lib/vault.client.ts @@ -7,7 +7,7 @@ import { createAccessToken, createVaultClient, } from "@thirdweb-dev/vault-sdk"; -import { updateProjectClient } from "../../../../../../../@3rdweb-sdk/react/hooks/useApi"; +import { updateProjectClient } from "../../../../../../../../@3rdweb-sdk/react/hooks/useApi"; const SERVER_WALLET_ACCESS_TOKEN_PURPOSE = "Access Token for All Server Wallets"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/page.tsx similarity index 94% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/page.tsx index eb332671b6c..86bd31a2d48 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/page.tsx @@ -3,7 +3,7 @@ import { getTeamBySlug } from "@/api/team"; import { THIRDWEB_VAULT_URL } from "@/constants/env"; import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk"; import { notFound, redirect } from "next/navigation"; -import { getAuthToken } from "../../../../api/lib/getAuthToken"; +import { getAuthToken } from "../../../../../api/lib/getAuthToken"; import { TransactionsAnalyticsPageContent } from "./analytics/analytics-page"; import { EngineChecklist } from "./analytics/ftux.client"; import { TransactionAnalyticsSummary } from "./analytics/summary"; @@ -50,7 +50,11 @@ export default async function TransactionsAnalyticsPage(props: { const vaultClient = await createVaultClient({ baseUrl: THIRDWEB_VAULT_URL, - }); + }).catch(() => undefined); + + if (!vaultClient) { + return
Error: Failed to connect to Vault
; + } const managementAccessToken = projectEngineCloudService?.managementAccessToken; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/create-server-wallet.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/create-server-wallet.client.tsx new file mode 100644 index 00000000000..ce18a9712bc --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/create-server-wallet.client.tsx @@ -0,0 +1,150 @@ +"use client"; +import type { Project } from "@/api/projects"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { useMutation } from "@tanstack/react-query"; +import { createEoa } from "@thirdweb-dev/vault-sdk"; +import { Loader2, WalletIcon } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { initVaultClient } from "../../lib/vault.client"; + +export default function CreateServerWallet(props: { + project: Project; + managementAccessToken: string | undefined; +}) { + const router = useDashboardRouter(); + const [label, setLabel] = useState(""); + const [modalOpen, setModalOpen] = useState(false); + + const createEoaMutation = useMutation({ + mutationFn: async ({ + managementAccessToken, + label, + }: { + managementAccessToken: string; + label: string; + }) => { + const vaultClient = await initVaultClient(); + + const eoa = await createEoa({ + request: { + options: { + metadata: { + projectId: props.project.id, + teamId: props.project.teamId, + type: "server-wallet", + label, + }, + }, + auth: { + accessToken: managementAccessToken, + }, + }, + client: vaultClient, + }); + + if (!eoa.success) { + throw new Error("Failed to create eoa"); + } + + router.refresh(); + setModalOpen(false); + + return eoa; + }, + onError: (error) => { + toast.error(error.message); + }, + }); + + const handleCreateServerWallet = async () => { + if (!props.managementAccessToken) { + router.push("vault"); + } else { + await createEoaMutation.mutateAsync({ + managementAccessToken: props.managementAccessToken, + label, + }); + } + }; + + const isLoading = createEoaMutation.isPending; + + return ( + <> + + + + + + Create server wallet + + Enter a label for your server wallet. + + +
+
+ setLabel(e.target.value)} + className="w-full" + /> +
+
+
+ + +
+
+
+ + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/try-it-out.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/try-it-out.tsx new file mode 100644 index 00000000000..455bbdd6270 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/components/try-it-out.tsx @@ -0,0 +1,303 @@ +"use client"; +import { THIRDWEB_ENGINE_CLOUD_URL } from "@/constants/env"; +import Link from "next/link"; +import { useState } from "react"; +import { Button } from "../../../../../../../../../@/components/ui/button"; +import { CodeClient } from "../../../../../../../../../@/components/ui/code/code.client"; +import { TabButtons } from "../../../../../../../../../@/components/ui/tabs"; +import type { Wallet } from "../wallet-table/types"; + +export function TryItOut(props: { + authToken: string; + wallet?: Wallet; + team_slug: string; + project_slug: string; +}) { + const [activeTab, setActiveTab] = useState("curl"); + + return ( +
+
+
+
+

+ Usage from your backend +

+

+ Send transactions from your server wallets using a simple http API +

+
+
+
+
+ setActiveTab("curl"), + isActive: activeTab === "curl", + }, + { + name: "JavaScript", + onClick: () => setActiveTab("js"), + isActive: activeTab === "js", + }, + { + name: "Python", + onClick: () => setActiveTab("python"), + isActive: activeTab === "python", + }, + { + name: "Go", + onClick: () => setActiveTab("go"), + isActive: activeTab === "go", + }, + { + name: "C#", + onClick: () => setActiveTab("csharp"), + isActive: activeTab === "csharp", + }, + ]} + /> + +
+ + {activeTab === "curl" && ( + + )} + {activeTab === "js" && ( + + )} + {activeTab === "python" && ( + + )} + {activeTab === "go" && ( + + )} + {activeTab === "csharp" && ( + + )} + +
+
+ + {props.wallet && ( + + )} +
+
+
+ ); +} + +const curlExample = () => `\ +curl -X POST "${THIRDWEB_ENGINE_CLOUD_URL}/write/contract" \\ + -H "Content-Type: application/json" \\ + -H "x-secret-key: " \\ + -H "x-vault-access-token: " \\ + -d '{ + "executionOptions": { + "type": "AA", + "signerAddress": "", + "chainId": "84532" + }, + "params": [ + { + "contractAddress": "0x...", + "method": "function mintTo(address to, uint256 amount)", + "params": ["0x...", "100"] + } + ] + }'`; + +const jsExample = () => `\ +const response = await fetch( + "${THIRDWEB_ENGINE_CLOUD_URL}/write/contract", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-secret-key": "", + "x-vault-access-token": "" + }, + body: JSON.stringify({ + "executionOptions": { + "type": "AA", + "signerAddress": "", + "chainId": "84532" + }, + "params": [ + { + "contractAddress": "0x...", + "method": "function mintTo(address to, uint256 amount)", + "params": ["0x...", "100"] + } + ] + }) + } +);`; + +const pythonExample = () => `\ +import requests +import json + +url = "${THIRDWEB_ENGINE_CLOUD_URL}/write/contract" +headers = { + "Content-Type": "application/json", + "x-secret-key": "", + "x-vault-access-token": "" +} +payload = { + "executionOptions": { + "type": "AA", + "signerAddress": "", + "chainId": "84532" + }, + "params": [ + { + "contractAddress": "0x...", + "method": "function mintTo(address to, uint256 amount)", + "params": ["0x...", "100"] + } + ] +} + +response = requests.post(url, headers=headers, json=payload) +result = response.json()`; + +const goExample = () => `\ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +func main() { + url := "${THIRDWEB_ENGINE_CLOUD_URL}/write/contract" + + // Create the request payload + type Param struct { + ContractAddress string \`json:"contractAddress"\` + Method string \`json:"method"\` + Params []string \`json:"params"\` + } + + type RequestBody struct { + ExecutionOptions struct { + Type string \`json:"type"\` + SignerAddress string \`json:"signerAddress"\` + ChainId string \`json:"chainId"\` + } \`json:"executionOptions"\` + Params []Param \`json:"params"\` + } + + requestBody := RequestBody{ + Params: []Param{ + { + ContractAddress: "0x...", + Method: "function mintTo(address to, uint256 amount)", + Params: []string{"0x...", "100"}, + }, + }, + } + requestBody.ExecutionOptions.Type = "AA" + requestBody.ExecutionOptions.SignerAddress = "" + requestBody.ExecutionOptions.ChainId = "84532" + + jsonData, _ := json.Marshal(requestBody) + + // Create the HTTP request + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-secret-key", "") + req.Header.Set("x-vault-access-token", "") + + // Send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error:", err) + return + } + defer resp.Body.Close() + + // Process the response + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + fmt.Println("Response:", result) +}`; + +const csharpExample = () => `\ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +class Program +{ + static async Task Main() + { + var url = "${THIRDWEB_ENGINE_CLOUD_URL}/write/contract"; + + var requestData = new + { + executionOptions = new + { + type = "AA", + signerAddress = "", + chainId = "84532" + }, + @params = new[] + { + new + { + contractAddress = "0x...", + method = "function mintTo(address to, uint256 amount)", + @params = new[] { "0x...", "100" } + } + } + }; + + var json = JsonSerializer.Serialize(requestData); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("x-secret-key", ""); + httpClient.DefaultRequestHeaders.Add("x-vault-access-token", ""); + + var response = await httpClient.PostAsync(url, content); + var responseContent = await response.Content.ReadAsStringAsync(); + + Console.WriteLine(responseContent); + } +}`; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/page.tsx similarity index 92% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/page.tsx index 368791001a8..5e853b4d840 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/page.tsx @@ -1,8 +1,8 @@ import { getProject } from "@/api/projects"; import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk"; import { notFound } from "next/navigation"; -import { THIRDWEB_VAULT_URL } from "../../../../../../../@/constants/env"; -import { getAuthToken } from "../../../../../api/lib/getAuthToken"; +import { THIRDWEB_VAULT_URL } from "../../../../../../../../@/constants/env"; +import { getAuthToken } from "../../../../../../api/lib/getAuthToken"; import { TryItOut } from "./components/try-it-out"; import type { Wallet } from "./wallet-table/types"; import { ServerWalletsTable } from "./wallet-table/wallet-table"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/wallet-table/types.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/wallet-table/types.ts similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/wallet-table/types.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/wallet-table/types.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/wallet-table/wallet-table-ui.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/wallet-table/wallet-table-ui.client.tsx similarity index 94% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/wallet-table/wallet-table-ui.client.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/wallet-table/wallet-table-ui.client.tsx index f017d1f283e..cc3a5b34c21 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/wallet-table/wallet-table-ui.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/wallet-table/wallet-table-ui.client.tsx @@ -2,6 +2,9 @@ import type { Project } from "@/api/projects"; import { WalletAddress } from "@/components/blocks/wallet-address"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; import { Table, TableBody, @@ -21,10 +24,7 @@ import { DEFAULT_ACCOUNT_FACTORY_V0_7, predictSmartAccountAddress, } from "thirdweb/wallets/smart"; -import { Badge } from "../../../../../../../../@/components/ui/badge"; -import { Skeleton } from "../../../../../../../../@/components/ui/skeleton"; -import { Switch } from "../../../../../../../../@/components/ui/switch"; -import { useV5DashboardChain } from "../../../../../../../../lib/v5-adapter"; +import { useV5DashboardChain } from "../../../../../../../../../lib/v5-adapter"; import CreateServerWallet from "../components/create-server-wallet.client"; import type { Wallet } from "./types"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/wallet-table/wallet-table.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/wallet-table/wallet-table.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/server-wallets/wallet-table/wallet-table.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/server-wallets/wallet-table/wallet-table.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/tx/[id]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/tx/[id]/layout.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/tx/[id]/layout.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/tx/[id]/layout.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/tx/[id]/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/tx/[id]/page.tsx similarity index 84% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/tx/[id]/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/tx/[id]/page.tsx index 365e84a1215..68843aa1313 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/tx/[id]/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/tx/[id]/page.tsx @@ -1,6 +1,6 @@ import { notFound, redirect } from "next/navigation"; -import { getProject } from "../../../../../../../../@/api/projects"; -import { CodeServer } from "../../../../../../../../@/components/ui/code/code.server"; +import { getProject } from "../../../../../../../../../@/api/projects"; +import { CodeServer } from "../../../../../../../../../@/components/ui/code/code.server"; import { getSingleTransaction } from "../../lib/analytics"; export default async function TransactionPage({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/components/create-vault-account.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/create-vault-account.client.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/components/create-vault-account.client.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/create-vault-account.client.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/components/key-management.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/key-management.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/components/key-management.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/key-management.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/components/list-access-tokens.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/list-access-tokens.client.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/components/list-access-tokens.client.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/list-access-tokens.client.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/rotate-admin-key.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/rotate-admin-key.client.tsx new file mode 100644 index 00000000000..d1634fc02d5 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/components/rotate-admin-key.client.tsx @@ -0,0 +1,322 @@ +"use client"; + +import type { Project } from "@/api/projects"; +import { CopyTextButton } from "@/components/ui/CopyTextButton"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { cn } from "@/lib/utils"; +import { useMutation } from "@tanstack/react-query"; +import { rotateServiceAccount } from "@thirdweb-dev/vault-sdk"; +import { + CheckIcon, + CircleAlertIcon, + DownloadIcon, + Loader2, + RefreshCcwIcon, +} from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { + createManagementAccessToken, + createWalletAccessToken, + initVaultClient, + maskSecret, +} from "../../lib/vault.client"; + +export default function RotateAdminKeyButton(props: { project: Project }) { + const [modalOpen, setModalOpen] = useState(false); + const [keysConfirmed, setKeysConfirmed] = useState(false); + const [keysDownloaded, setKeysDownloaded] = useState(false); + const router = useDashboardRouter(); + + const rotateAdminKeyMutation = useMutation({ + mutationFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const vaultClient = await initVaultClient(); + const rotationCode = props.project.services.find( + (service) => service.name === "engineCloud", + )?.rotationCode; + + if (!rotationCode) { + throw new Error("Rotation code not found"); + } + + const rotateServiceAccountRes = await rotateServiceAccount({ + client: vaultClient, + request: { + auth: { + rotationCode, + }, + }, + }); + + if (rotateServiceAccountRes.error) { + throw new Error(rotateServiceAccountRes.error.message); + } + + // need to recreate the management access token with the new admin key + const managementAccessTokenPromise = createManagementAccessToken({ + project: props.project, + adminKey: rotateServiceAccountRes.data.newAdminKey, + rotationCode: rotateServiceAccountRes.data.newRotationCode, + vaultClient, + }); + + const userAccesTokenPromise = createWalletAccessToken({ + project: props.project, + adminKey: rotateServiceAccountRes.data.newAdminKey, + vaultClient, + }); + + const [userAccessTokenRes, managementAccessTokenRes] = await Promise.all([ + userAccesTokenPromise, + managementAccessTokenPromise, + ]); + + if (!managementAccessTokenRes.success || !userAccessTokenRes.success) { + throw new Error("Failed to create access token"); + } + + return { + success: true, + adminKey: rotateServiceAccountRes.data.newAdminKey, + userAccessToken: userAccessTokenRes.data, + }; + }, + onError: (error) => { + toast.error(error.message); + }, + }); + + const handleDownloadKeys = () => { + if (!rotateAdminKeyMutation.data) { + return; + } + + const fileContent = `Project:\n${props.project.name} (${props.project.publishableKey})\n\nVault Admin Key:\n${rotateAdminKeyMutation.data.adminKey}\n\nVault Access Token:\n${rotateAdminKeyMutation.data.userAccessToken.accessToken}\n`; + const blob = new Blob([fileContent], { type: "text/plain;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + const filename = `${props.project.name}-vault-keys-rotated.txt`; + link.href = url; + link.download = filename; + document.body.appendChild(link); // Required for Firefox + link.click(); + + // Clean up + document.body.removeChild(link); + URL.revokeObjectURL(url); + + toast.success(`Keys downloaded as ${filename}`); + setKeysDownloaded(true); + }; + + const handleCloseModal = () => { + if (!keysConfirmed) { + return; + } + + setModalOpen(false); + setKeysConfirmed(false); + setKeysDownloaded(false); + // invalidate the page to force a reload + rotateAdminKeyMutation.reset(); + router.refresh(); + }; + + const isLoading = rotateAdminKeyMutation.isPending; + + return ( + <> + + + + + {rotateAdminKeyMutation.isPending ? ( + <> + + Generating new keys... + +
+ +

+ This may take a few seconds. +

+
+ + ) : rotateAdminKeyMutation.data ? ( +
+ + New Vault Keys + + +
+
+
+

+ New Vault Admin Key +

+
+ +

+ This key is used to create or revoke your access tokens. +

+
+
+ +
+

+ New Vault Access Token +

+
+ +

+ This access token is used to sign transactions and + messages from your backend. Can be revoked and recreated + with your admin key. +

+
+
+
+ + Secure your keys + + These keys will not be displayed again. Store them securely + as they provide access to your server wallets. + +
+
+ + {keysDownloaded && ( + + + + )} +
+
+ + setKeysConfirmed(!!v)} + /> + I confirm that I've securely stored these keys + + +
+ +
+ +
+
+ ) : ( + <> + + Rotate your Vault admin key + + This action will generate a new Vault admin key and rotation + code.{" "} + + +
+
+

+ Revoke your current keys and generates new ones. +

+ + + Important + + This action will invalidate your current admin key and all + existing access tokens. You will need to update your + backend to use these new access tokens. + + +
+
+ + +
+
+ + )} + +
+ + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/page.tsx similarity index 92% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/page.tsx index 149cb1dfa24..02857a44b72 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/vault/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/(overview)/vault/page.tsx @@ -1,6 +1,6 @@ import { getProject } from "@/api/projects"; import { notFound } from "next/navigation"; -import { getAuthToken } from "../../../../../api/lib/getAuthToken"; +import { getAuthToken } from "../../../../../../api/lib/getAuthToken"; import { KeyManagement } from "./components/key-management"; export default async function VaultPage(props: { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/EngineFooterCard.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/EngineFooterCard.stories.tsx new file mode 100644 index 00000000000..22a678e32f1 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/EngineFooterCard.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { EngineFooterCard } from "./_components"; + +const meta = { + title: "Engine/EngineFooterCard", + component: EngineFooterCard, + args: { + team_slug: "demo-team", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Growth: Story = { + args: { + teamPlan: "growth", + }, +}; + +export const Accelerate: Story = { + args: { + teamPlan: "accelerate", + }, +}; + +export const Scale: Story = { + args: { + teamPlan: "scale", + }, +}; + +export const Pro: Story = { + args: { + teamPlan: "pro", + }, +}; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/_components.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/_components.tsx new file mode 100644 index 00000000000..8c6680521a8 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/_components.tsx @@ -0,0 +1,167 @@ +import type { Team } from "@/api/team"; +import { UnderlineLink } from "@/components/ui/UnderlineLink"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { TrackedLinkTW } from "@/components/ui/tracked-link"; +import { PRO_CONTACT_US_URL } from "constants/pro"; +import { ArrowRightIcon, DownloadIcon, ExternalLinkIcon } from "lucide-react"; +import Link from "next/link"; + +export function ImportEngineLink(props: { + label: string; + engineLinkPrefix: string; +}) { + return ( + + ); +} + +function EngineInfoSection(props: { team_slug: string }) { + const engineLinkPrefix = `/team/${props.team_slug}/~/engine`; + + return ( +
+

+ What is Engine? +

+ +
    +
  • Read, write, and deploy contracts at production scale
  • +
  • + Reliably parallelize and retry transactions with gas & nonce + management +
  • +
  • Securely manage backend wallets
  • +
  • Built-in support for account abstraction, relayers, and more
  • +
+ +
+ + + +
+
+ ); +} + +function CloudHostedEngineSection(props: { + teamPlan: Exclude; + teamSlug: string; +}) { + return ( +
+

+ Get Managed Engine +

+ + {props.teamPlan !== "pro" ? ( +
+

+ Upgrade your plan to{" "} + + Accelerate + {" "} + or{" "} + + Scale + {" "} + to get a managed Engine instance +

+ +
+
+ + + +
+
+ ) : ( +
+

+ Contact us to get a managed engine for your team +

+ +
+ )} +
+ ); +} + +export function EngineFooterCard(props: { + teamPlan: Team["billingPlan"]; + team_slug: string; +}) { + return ( +
+ {props.teamPlan === "accelerate" || props.teamPlan === "scale" ? null : ( + <> + + + + )} + + +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.stories.tsx new file mode 100644 index 00000000000..4e3307be5c4 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { EngineImportCardUI } from "./EngineImportPage"; + +const meta: Meta = { + title: "Engine/general/import", + component: EngineImportCardUI, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + prefillImportUrl: undefined, + importEngine: async (params) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("importEngine", params); + }, + }, +}; + +export const WithPrefillUrl: Story = { + args: { + prefillImportUrl: "https://engine.example.com", + importEngine: async (params) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("importEngine", params); + }, + }, +}; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.tsx new file mode 100644 index 00000000000..ea9f330bc2b --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/EngineImportPage.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { apiServerProxy } from "@/actions/proxies"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { TrackedLinkTW } from "@/components/ui/tracked-link"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { FormControl } from "@chakra-ui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { CircleAlertIcon, DownloadIcon, ExternalLinkIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + url: z.string().url("Please enter a valid URL").min(1, "URL is required"), +}); + +type ImportEngineParams = z.infer; + +async function importEngine({ + teamIdOrSlug, + ...data +}: ImportEngineParams & { teamIdOrSlug: string }) { + // Instance URLs should end with a /. + const url = data.url.endsWith("/") ? data.url : `${data.url}/`; + + const res = await apiServerProxy({ + pathname: `/v1/teams/${teamIdOrSlug}/engine`, + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: data.name, + url, + }), + }); + + if (!res.ok) { + throw new Error(res.error); + } +} + +export function EngineImportCard(props: { + prefillImportUrl: string | undefined; + teamSlug: string; +}) { + const router = useDashboardRouter(); + + return ( + { + await importEngine({ ...params, teamIdOrSlug: props.teamSlug }); + router.push(`/team/${props.teamSlug}/~/engine`); + }} + /> + ); +} + +export function EngineImportCardUI(props: { + prefillImportUrl: string | undefined; + importEngine: (params: ImportEngineParams) => Promise; +}) { + const form = useForm({ + resolver: zodResolver(formSchema), + values: { + name: "", + url: props.prefillImportUrl || "", + }, + }); + + const importMutation = useMutation({ + mutationFn: props.importEngine, + }); + + const onSubmit = async (data: ImportEngineParams) => { + try { + await importMutation.mutateAsync(data); + toast.success("Engine imported successfully"); + } catch (e) { + const message = e instanceof Error ? e.message : undefined; + toast.error( + "Error importing Engine. Please check if the details are correct.", + { + description: message, + }, + ); + } + }; + + return ( +
+
+ + {/* Card */} +
+
+

+ Import Engine Instance +

+ +

+ Import an Engine instance hosted on your infrastructure +

+ +
+ + + Get help setting up Engine for free + + +
+ +
+ ( + + Name + + + + + + )} + /> + + ( + + URL + + + + +
+ +

+ Do not import a URL you do not recognize. +

+
+
+ )} + /> +
+
+ +
+ +
+ +
+ + +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/page.tsx new file mode 100644 index 00000000000..89d16708101 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/import/page.tsx @@ -0,0 +1,27 @@ +import { EngineImportCard } from "./EngineImportPage"; + +export default async function Page(props: { + params: Promise<{ team_slug: string }>; + searchParams: Promise<{ + importUrl?: string | string[]; + }>; +}) { + const [params, searchParams] = await Promise.all([ + props.params, + props.searchParams, + ]); + + const importUrl = + typeof searchParams.importUrl === "string" + ? searchParams.importUrl + : undefined; + + return ( +
+ +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/layout.tsx new file mode 100644 index 00000000000..c3105737adc --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/layout.tsx @@ -0,0 +1,113 @@ +import type { SidebarLink } from "@/components/blocks/Sidebar"; +import { SidebarLayout } from "@/components/blocks/SidebarLayout"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { DatabaseIcon } from "lucide-react"; +import Link from "next/link"; +import { EngineIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon"; +import { ImportEngineLink } from "./_components"; + +export default async function Layout(props: { + params: Promise<{ + team_slug: string; + project_slug: string; + }>; + children: React.ReactNode; +}) { + const params = await props.params; + const linkPrefix = `/team/${params.team_slug}/${params.project_slug}/engine/legacy`; + const sidebarLinks: SidebarLink[] = [ + { + label: "Engine Instances", + href: `${linkPrefix}`, + exactMatch: true, + }, + { + label: "Import Engine", + href: `${linkPrefix}/import`, + }, + ]; + + return ( +
+ {/* header */} +
+
+
+

+ Engine{" "} + + Dedicated + +

+

+ Manage your deployed Engine instances. +

+
+
+ +
+
+
+
+ +
+
+ + {/* sidebar layout */} + + {props.children} + +
+ ); +} + +function EngineLegacyBannerUI(props: { + teamSlug: string; + projectSlug: string; +}) { + return ( + + + Engine Cloud (Beta) + +
+

+ Try Engine Cloud (Beta) - now included for free in every thirdweb + project. +

+
+
    +
  • No recurring monthly cost, pay-per-request model
  • +
  • Powered by Vault: our new TEE based key management system
  • +
  • Improved performance and simplified transaction API
  • +
+
+
+ {/* TODO (cloud): add link to Engine Cloud blog post */} + + + + + + +
+ + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.stories.tsx new file mode 100644 index 00000000000..b5f703bcfa5 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.stories.tsx @@ -0,0 +1,166 @@ +import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine"; +import type { Meta, StoryObj } from "@storybook/react"; +import { EngineInstancesTableUI } from "./engine-instances-table"; + +const meta: Meta = { + title: "Engine/general/instances", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, + decorators: [ + (StoryInstance) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +function createEngineInstanceStub( + name: string, + status: EngineInstance["status"], + isCloudHosted: boolean, + isPlanEngine: boolean, +): EngineInstance { + const engineId = `${name.toLowerCase().replace(/\s+/g, "-")}-engine`; + return { + id: engineId, + name, + url: `https://${engineId}.example.com`, + status, + deploymentId: status === "active" ? "dep-123" : undefined, + accountId: "acc-123", + lastAccessedAt: new Date().toISOString(), + isCloudHosted, + isPlanEngine, + }; +} + +const cloudHostedEngineInstance = createEngineInstanceStub( + "Cloud Hosted Engine", + "active", + true, + false, +); + +const cloudHostedPlanActiveEngineInstance = createEngineInstanceStub( + "Cloud Hosted Plan Engine", + "active", + true, + true, +); +const selfHostedActiveEngineInstance = createEngineInstanceStub( + "Self Hosted Engine", + "active", + false, + false, +); +const pendingEngineInstance = createEngineInstanceStub( + "Pending Engine", + "requested", + false, + false, +); +const deployingEngineInstance = createEngineInstanceStub( + "Deploying Engine", + "deploying", + false, + false, +); + +const paymentFailedEngineInstance = createEngineInstanceStub( + "Payment Failed Engine", + "paymentFailed", + false, + false, +); + +const deploymentFailedEngineInstance = createEngineInstanceStub( + "Deployment Failed Engine", + "deploymentFailed", + false, + false, +); + +export const MultipleInstances: Story = { + args: { + instances: [ + // active + cloudHostedEngineInstance, + cloudHostedPlanActiveEngineInstance, + selfHostedActiveEngineInstance, + // others + pendingEngineInstance, + deployingEngineInstance, + paymentFailedEngineInstance, + deploymentFailedEngineInstance, + ], + engineLinkPrefix: "/team/test/engine", + }, +}; + +export const NoInstancesProPlan: Story = { + args: { + instances: [], + engineLinkPrefix: "/team/test/engine", + teamPlan: "pro", + }, +}; + +export const NoInstancesGrowthPlan: Story = { + args: { + instances: [], + engineLinkPrefix: "/team/test/engine", + teamPlan: "growth", + }, +}; + +// this one can't technically happen because Accelerate plan always has one cloud hosted engine by default - but testing it anyway +// the section that prompts user to either choose a cloud-hosted engine or import an engine is hidden in this case +export const NoInstancesAcceleratePlan: Story = { + args: { + instances: [], + engineLinkPrefix: "/team/test/engine", + teamPlan: "accelerate", + }, +}; + +export const OneInstance: Story = { + args: { + instances: [cloudHostedEngineInstance], + engineLinkPrefix: "/team/test/engine", + }, +}; + +function Story( + props: Omit< + React.ComponentProps, + | "deleteCloudHostedEngine" + | "editEngineInstance" + | "removeEngineFromDashboard" + >, +) { + return ( + { + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("deleteCloudHostedEngine", params); + }} + editEngineInstance={async (params) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("editEngineInstance", params); + }} + removeEngineFromDashboard={async (params) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("removeEngineFromDashboard", params); + }} + {...props} + /> + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.tsx new file mode 100644 index 00000000000..92ee878b9ce --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/engine/legacy/(general)/overview/engine-instances-table.tsx @@ -0,0 +1,938 @@ +"use client"; + +import type { Team } from "@/api/team"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { UnderlineLink } from "@/components/ui/UnderlineLink"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge, type BadgeProps } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Textarea } from "@/components/ui/textarea"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { cn } from "@/lib/utils"; +import { + type DeleteCloudHostedEngineParams, + type EditEngineInstanceParams, + type EngineInstance, + type RemoveEngineFromDashboardIParams, + deleteCloudHostedEngine, + editEngineInstance, + removeEngineFromDashboard, +} from "@3rdweb-sdk/react/hooks/useEngine"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { PRO_CONTACT_US_URL } from "constants/pro"; +import { useTrack } from "hooks/analytics/useTrack"; +import { + CheckIcon, + CircleAlertIcon, + InfoIcon, + PencilIcon, + Trash2Icon, +} from "lucide-react"; +import { MoreHorizontalIcon } from "lucide-react"; +import { ArrowRight } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { EngineIcon } from "../../../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon"; + +type DeletedCloudHostedEngine = ( + params: DeleteCloudHostedEngineParams, +) => Promise; + +type EditedEngineInstance = (params: EditEngineInstanceParams) => Promise; + +type RemovedEngineFromDashboard = ( + params: RemoveEngineFromDashboardIParams, +) => Promise; + +export function EngineInstancesTable(props: { + teamSlug: string; + instances: EngineInstance[]; + engineLinkPrefix: string; + teamPlan: Team["billingPlan"]; +}) { + const router = useDashboardRouter(); + + return ( + { + await deleteCloudHostedEngine(params); + router.refresh(); + }} + editEngineInstance={async (params) => { + await editEngineInstance(params); + router.refresh(); + }} + removeEngineFromDashboard={async (params) => { + await removeEngineFromDashboard(params); + router.refresh(); + }} + /> + ); +} + +export function EngineInstancesTableUI(props: { + instances: EngineInstance[]; + engineLinkPrefix: string; + deleteCloudHostedEngine: DeletedCloudHostedEngine; + editEngineInstance: EditedEngineInstance; + removeEngineFromDashboard: RemovedEngineFromDashboard; + teamPlan: Team["billingPlan"]; + teamSlug: string; +}) { + return ( +
+

+ Engine Instances +

+ + {props.instances.length === 0 ? ( + + ) : ( + + + + + Engine Instance + Version + Actions + + + + {props.instances.map((instance) => ( + + ))} + +
+
+ )} +
+ ); +} + +function EngineInstanceRow(props: { + teamIdOrSlug: string; + instance: EngineInstance; + engineLinkPrefix: string; + deleteCloudHostedEngine: DeletedCloudHostedEngine; + editEngineInstance: EditedEngineInstance; + removeEngineFromDashboard: RemovedEngineFromDashboard; +}) { + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false); + const { instance, engineLinkPrefix } = props; + + return ( + <> + + +
+
+ + +
+ {instance.status !== "active" && ( +
+ +
+ )} +
+
+ + v2 + + + setIsEditModalOpen(true)} + onRemove={() => setIsRemoveModalOpen(true)} + /> + +
+ + + + + + ); +} + +function InstanceNameLink(props: { + instance: EngineInstance; + engineLinkPrefix: string; +}) { + const name = ( + + {props.instance.name} + + ); + return ( +
+ {props.instance.status === "requested" || + props.instance.status === "deploying" || + props.instance.status === "deploymentFailed" || + props.instance.status === "paymentFailed" ? ( + {name} + ) : ( + + {name} + + )} +
+ ); +} + +function EngineURL(props: { url: string }) { + const cleanedURL = props.url.endsWith("/") + ? props.url.slice(0, -1) + : props.url; + + return

{cleanedURL}

; +} + +const engineStatusMeta: Record< + EngineInstance["status"], + { + label: string; + variant: BadgeProps["variant"]; + icon: React.FC<{ className?: string }>; + } +> = { + requested: { + label: "Pending", + variant: "outline", + icon: Spinner, + }, + deploying: { + label: "Deploying", + variant: "default", + icon: Spinner, + }, + active: { + label: "Active", + variant: "default", + icon: CheckIcon, + }, + pending: { + label: "Pending", + variant: "outline", + icon: Spinner, + }, + paymentFailed: { + label: "Payment Failed", + variant: "destructive", + icon: CircleAlertIcon, + }, + deploymentFailed: { + label: "Deployment Failed", + variant: "destructive", + icon: CircleAlertIcon, + }, +}; + +function EngineStatusBadge(props: { + status: EngineInstance["status"]; +}) { + const statusMeta = engineStatusMeta[props.status]; + return ( + + + {statusMeta.label} + + ); +} + +function EngineActionsDropdown(props: { + instance: EngineInstance; + onEdit: (instance: EngineInstance) => void; + onRemove: (instance: EngineInstance) => void; +}) { + const trackEvent = useTrack(); + const canDelete = + props.instance.status === "paymentFailed" || + props.instance.status === "deploymentFailed" || + props.instance.status === "active" || + !!props.instance.deploymentId; + + return ( + + + + + + { + trackEvent({ + category: "engine", + action: "edit", + label: "open-modal", + }); + props.onEdit(props.instance); + }} + > + + Edit + + + {/* plan engine is not removable */} + {!props.instance.isPlanEngine && ( + { + trackEvent({ + category: "engine", + action: "remove", + label: "open-modal", + }); + props.onRemove(props.instance); + }} + > + + Delete + + )} + + + ); +} + +function EditModal(props: { + open: boolean; + teamIdOrSlug: string; + onOpenChange: (open: boolean) => void; + instance: EngineInstance; + editEngineInstance: EditedEngineInstance; +}) { + return ( + + + props.onOpenChange(false)} + /> + + + ); +} + +const editEngineFormSchema = z.object({ + name: z.string().min(1, "Name is required"), + url: z.string().url("Invalid URL"), +}); + +function EditModalContent(props: { + teamIdOrSlug: string; + instance: EngineInstance; + editEngineInstance: EditedEngineInstance; + closeModal: () => void; +}) { + const editInstance = useMutation({ + mutationFn: props.editEngineInstance, + }); + const { instance } = props; + + const form = useForm>({ + resolver: zodResolver(editEngineFormSchema), + values: { + name: instance.name, + url: instance.url, + }, + reValidateMode: "onChange", + }); + + return ( +
+ + editInstance.mutate( + { + teamIdOrSlug: props.teamIdOrSlug, + instanceId: props.instance.id, + name: data.name, + url: data.url, + }, + { + onSuccess: () => { + toast.success("Engine updated successfully"); + props.closeModal(); + }, + onError: () => { + toast.error("Failed to update Engine"); + }, + }, + ), + )} + > +
+ + Edit Engine Instance + + + ( + + Name + + + + + + )} + /> + + {/* cloud hosted engine url is not editable */} + {!props.instance.isCloudHosted && ( + ( + + URL + + + + + + )} + /> + )} +
+ +
+ + +
+
+ + ); +} + +function RemoveModal(props: { + teamIdOrSlug: string; + instance: EngineInstance; + open: boolean; + onOpenChange: (open: boolean) => void; + deleteCloudHostedEngine: DeletedCloudHostedEngine; + removeEngineFromDashboard: RemovedEngineFromDashboard; +}) { + const { instance, open, onOpenChange } = props; + + return ( + + + {instance.status === "paymentFailed" || + instance.status === "deploymentFailed" || + (instance.status === "active" && !instance.deploymentId) ? ( + onOpenChange(false)} + removeEngineFromDashboard={props.removeEngineFromDashboard} + /> + ) : instance.deploymentId ? ( + onOpenChange(false)} + deleteCloudHostedEngine={props.deleteCloudHostedEngine} + /> + ) : null} + + + ); +} + +function RemoveEngineFromDashboardModalContent(props: { + teamIdOrSlug: string; + instance: EngineInstance; + close: () => void; + removeEngineFromDashboard: RemovedEngineFromDashboard; +}) { + const { instance, close } = props; + const removeFromDashboard = useMutation({ + mutationFn: props.removeEngineFromDashboard, + }); + + return ( +
+
+ + Remove Engine from Dashboard + + + Are you sure you want to remove{" "} + {instance.name} from + your dashboard? + + + + +
+ + + + + This action does not modify your Engine infrastructure + + + You can import engine to dashboard again later + + +
+ +
+ + +
+
+ ); +} + +const deleteEngineReasons: Array<{ + value: DeleteCloudHostedEngineParams["reason"]; + label: string; +}> = [ + { value: "USING_SELF_HOSTED", label: "Migrating to self-hosted" }, + { value: "TOO_EXPENSIVE", label: "Too expensive" }, + { value: "MISSING_FEATURES", label: "Missing features" }, + { value: "OTHER", label: "Other" }, +]; + +const deleteEngineFormSchema = z.object({ + reason: z.enum([ + "USING_SELF_HOSTED", + "TOO_EXPENSIVE", + "MISSING_FEATURES", + "OTHER", + ]), + feedback: z.string(), + confirmDeletion: z.boolean(), +}); + +function DeleteEngineSubscriptionModalContent(props: { + instance: EngineInstance; + close: () => void; + deleteCloudHostedEngine: DeletedCloudHostedEngine; +}) { + const { instance, close } = props; + const deleteCloudHostedEngine = useMutation({ + mutationFn: props.deleteCloudHostedEngine, + }); + + const form = useForm>({ + resolver: zodResolver(deleteEngineFormSchema), + defaultValues: { + feedback: "", + confirmDeletion: false, + }, + reValidateMode: "onChange", + }); + + const onSubmit = (data: z.infer) => { + // unexpected state + if (!instance.deploymentId) { + toast.error("Can not delete this Engine instance", { + description: "Engine instance is missing deployment id", + }); + return; + } + + deleteCloudHostedEngine.mutate( + { + deploymentId: instance.deploymentId, + reason: data.reason, + feedback: data.feedback, + }, + { + onSuccess: () => { + toast.success( + "Deleting Engine. Please check again in a few minutes.", + { + dismissible: true, + duration: 10000, + }, + ); + + close(); + }, + onError: () => { + toast.error( + "Error deleting Engine. Please visit https://thirdweb.com/support.", + ); + }, + }, + ); + }; + + return ( +
+
+ +
+ + Permanently Delete Engine + + +

+ This step will cancel your monthly subscription and immediately + delete all data and infrastructure for this Engine. +

+ + {/* Reason */} + ( + + + Please share your feedback to help us improve Engine. + + + + {deleteEngineReasons.map((reason) => ( + + + + + + {reason.label} + + + ))} + + + + + )} + /> + +
+ + {/* Feedback */} + ( + + +