diff --git a/.commitlintrc.json b/.commitlintrc.json index 60b0b0b68..8ad736340 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -21,7 +21,8 @@ "repo", "styling", "observability", - "analytics" + "analytics", + "template" ] ] } diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 3b175adff..ab0362cb5 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,48 @@ +## [2.35.2](https://github.com/akash-network/console/compare/console-api/v2.35.2-beta.0...console-api/v2.35.2) (2024-12-02) + +## [2.35.2-beta.0](https://github.com/akash-network/console/compare/console-api/v2.35.1...console-api/v2.35.2-beta.0) (2024-11-28) + + +### Bug Fixes + +* **observability:** ensure pino-pretty works in built app ([7f6f9ca](https://github.com/akash-network/console/commit/7f6f9ca7ca4e1ff4bc3b85735270f61cc8120242)), closes [#474](https://github.com/akash-network/console/issues/474) + +## [2.35.1](https://github.com/akash-network/console/compare/console-api/v2.35.1-beta.1...console-api/v2.35.1) (2024-11-28) + +## [2.35.1-beta.1](https://github.com/akash-network/console/compare/console-api/v2.35.1-beta.0...console-api/v2.35.1-beta.1) (2024-11-28) + + +### Bug Fixes + +* **deployment:** provider deployments query fix ([4278bbd](https://github.com/akash-network/console/commit/4278bbd718d56a71d49baefd73d1b2d35e427aff)), closes [#504](https://github.com/akash-network/console/issues/504) + +## [2.35.1-beta.0](https://github.com/akash-network/console/compare/console-api/v2.35.0...console-api/v2.35.1-beta.0) (2024-11-28) + + +### Bug Fixes + +* **deployment:** fix console arg to object mapping ([6126106](https://github.com/akash-network/console/commit/6126106a800d7006b726ff98190e09368cc0c130)), closes [#503](https://github.com/akash-network/console/issues/503) + +## [2.35.0](https://github.com/akash-network/console/compare/console-api/v2.35.0-beta.0...console-api/v2.35.0) (2024-11-27) + +## [2.35.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.34.0...console-api/v2.35.0-beta.0) (2024-11-27) + + +### Features + +* **deployment:** clean up trial deployments for a provider ([41018af](https://github.com/akash-network/console/commit/41018afc0593621c4627369b9f114f849e249e44)), closes [#502](https://github.com/akash-network/console/issues/502) + +## [2.34.0](https://github.com/akash-network/console/compare/console-api/v2.34.0-beta.1...console-api/v2.34.0) (2024-11-26) + +## [2.34.0-beta.1](https://github.com/akash-network/console/compare/console-api/v2.34.0-beta.0...console-api/v2.34.0-beta.1) (2024-11-26) + + +### Features + +* **deployment:** implement ato top up setting ([1301314](https://github.com/akash-network/console/commit/130131485a68f699587415f96283e0dc83072502)), closes [#412](https://github.com/akash-network/console/issues/412) + ## [2.34.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.33.1...console-api/v2.34.0-beta.0) (2024-11-23) diff --git a/apps/api/env/.env b/apps/api/env/.env index d99c3f9df..6b2d3843c 100644 --- a/apps/api/env/.env +++ b/apps/api/env/.env @@ -1,4 +1,4 @@ -CORS_WEBSITE_URLS=https://stats.akash.network,https://console.akash.network,https://akash.network,https://akash.hooman.digital,http://localhost:3000,http://localhost:3001,https://akashconsole.vercel.app,https://console-beta.akash.network +CORS_WEBSITE_URLS=https://stats.akash.network,https://console.akash.network,https://akash.network,https://akash.hooman.digital,http://localhost:3000,http://localhost:3001,https://akashconsole.vercel.app,https://console-beta.akash.network,https://provider-console-beta.akash.network,https://provider-console.akash.network WEBSITE_URL=https://console-beta.akash.network TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT=10000000 DEPLOYMENT_ALLOWANCE_REFILL_AMOUNT=10000000 diff --git a/apps/api/mvm.lock b/apps/api/mvm.lock index e3c3f96a8..eeba268dd 100644 --- a/apps/api/mvm.lock +++ b/apps/api/mvm.lock @@ -3,6 +3,6 @@ "@akashnetwork/database": "1.0.0", "@akashnetwork/env-loader": "1.0.1", "@akashnetwork/http-sdk": "1.0.8", - "@akashnetwork/logging": "2.0.1" + "@akashnetwork/logging": "2.0.2" } } diff --git a/apps/api/package.json b/apps/api/package.json index e198ba80a..75014f3f2 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.34.0-beta.0", + "version": "2.35.2", "description": "Api providing data to the deploy tool", "repository": { "type": "git", @@ -54,9 +54,9 @@ "@hono/swagger-ui": "0.2.1", "@hono/zod-openapi": "0.9.5", "@octokit/rest": "^18.12.0", - "@opentelemetry/instrumentation": "^0.54.0", - "@opentelemetry/instrumentation-http": "^0.54.0", - "@opentelemetry/sdk-node": "^0.54.0", + "@opentelemetry/instrumentation": "^0.54.2", + "@opentelemetry/instrumentation-http": "^0.54.2", + "@opentelemetry/sdk-node": "^0.54.2", "@sentry/node": "^7.55.2", "@supercharge/promise-pool": "^3.2.0", "@ucast/core": "^1.10.2", diff --git a/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts b/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts new file mode 100644 index 000000000..111697400 --- /dev/null +++ b/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts @@ -0,0 +1,90 @@ +import { LoggerService } from "@akashnetwork/logging"; +import { singleton } from "tsyringe"; + +import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; +import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; +import { ManagedUserWalletService, RpcMessageService } from "@src/billing/services"; +import { ErrorService } from "@src/core/services/error/error.service"; +import { ProviderCleanupSummarizer } from "@src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer"; +import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository"; +import { TxSignerService } from "../tx-signer/tx-signer.service"; + +export interface ProviderCleanupParams { + concurrency: number; + provider: string; + dryRun: boolean; +} + +@singleton() +export class ProviderCleanupService { + private readonly logger = LoggerService.forContext(ProviderCleanupService.name); + + constructor( + @InjectBillingConfig() private readonly config: BillingConfig, + private readonly userWalletRepository: UserWalletRepository, + private readonly managedUserWalletService: ManagedUserWalletService, + private readonly txSignerService: TxSignerService, + private readonly deploymentRepository: DeploymentRepository, + private readonly rpcMessageService: RpcMessageService, + private readonly errorService: ErrorService + ) {} + + async cleanup(options: ProviderCleanupParams) { + const summary = new ProviderCleanupSummarizer(); + await this.userWalletRepository.paginate({ query: { isTrialing: true }, limit: options.concurrency || 10 }, async wallets => { + const cleanUpAllWallets = wallets.map(async wallet => { + await this.errorService.execWithErrorHandler( + { + wallet, + event: "PROVIDER_CLEAN_UP_ERROR", + context: ProviderCleanupService.name + }, + () => this.cleanUpForWallet(wallet, options, summary) + ); + }); + + await Promise.all(cleanUpAllWallets); + }); + + this.logger.info({ event: "PROVIDER_CLEAN_UP_SUMMARY", summary: summary.summarize(), dryRun: options.dryRun }); + } + + private async cleanUpForWallet(wallet: UserWalletOutput, options: ProviderCleanupParams, summary: ProviderCleanupSummarizer) { + const client = await this.txSignerService.getClientForAddressIndex(wallet.id); + const deployments = await this.deploymentRepository.findDeploymentsForProvider({ + owner: wallet.address, + provider: options.provider + }); + + const closeAllWalletStaleDeployments = deployments.map(async deployment => { + const message = this.rpcMessageService.getCloseDeploymentMsg(wallet.address, deployment.dseq); + this.logger.info({ event: "PROVIDER_CLEAN_UP", params: { owner: wallet.address, dseq: deployment.dseq } }); + + try { + if (!options.dryRun) { + await client.signAndBroadcast([message]); + this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" }); + } + } catch (error) { + if (error.message.includes("not allowed to pay fees")) { + if (!options.dryRun) { + await this.managedUserWalletService.authorizeSpending({ + address: wallet.address, + limits: { + fees: this.config.FEE_ALLOWANCE_REFILL_AMOUNT + } + }); + await client.signAndBroadcast([message]); + this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" }); + } + } else { + throw error; + } + } finally { + summary.inc("deploymentCount"); + } + }); + + await Promise.all(closeAllWalletStaleDeployments); + } +} diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index e2ec98ff6..115bc32b1 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -14,6 +14,7 @@ import { chainDb } from "@src/db/dbConnection"; import { TopUpDeploymentsController } from "@src/deployment/controllers/deployment/deployment.controller"; import { UserController } from "@src/user/controllers/user/user.controller"; import { UserConfigService } from "@src/user/services/user-config/user-config.service"; +import { ProviderController } from "./deployment/controllers/provider/provider.controller"; const program = new Command(); @@ -42,13 +43,25 @@ program program .command("cleanup-stale-deployments") .description("Close deployments without leases created at least 10min ago") - .option("-c, --concurrency ", "How much wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value)) + .option("-c, --concurrency ", "How many wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value)) .action(async (options, command) => { await executeCliHandler(command.name(), async () => { await container.resolve(TopUpDeploymentsController).cleanUpStaleDeployment(options); }); }); +program + .command("cleanup-provider-deployments") + .description("Close trial deployments for a provider") + .option("-c, --concurrency ", "How many wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value)) + .option("-d, --dry-run", "Dry run the trial provider cleanup", false) + .option("-p, --provider ", "Provider address", value => z.string().parse(value)) + .action(async (options, command) => { + await executeCliHandler(command.name(), async () => { + await container.resolve(ProviderController).cleanupProviderDeployments(options); + }); + }); + const userConfig = container.resolve(UserConfigService); program .command("cleanup-stale-anonymous-users") diff --git a/apps/api/src/deployment/controllers/provider/provider.controller.ts b/apps/api/src/deployment/controllers/provider/provider.controller.ts index d2c6261d8..de90d8154 100644 --- a/apps/api/src/deployment/controllers/provider/provider.controller.ts +++ b/apps/api/src/deployment/controllers/provider/provider.controller.ts @@ -1,12 +1,20 @@ import { singleton } from "tsyringe"; +import { ProviderCleanupParams, ProviderCleanupService } from "@src/billing/services/provider-cleanup/provider-cleanup.service"; import { TrialProvidersService } from "@src/deployment/services/trial-providers/trial-providers.service"; @singleton() export class ProviderController { - constructor(private readonly trialProvidersService: TrialProvidersService) {} + constructor( + private readonly trialProvidersService: TrialProvidersService, + private readonly providerCleanupService: ProviderCleanupService + ) {} async getTrialProviders(): Promise { return await this.trialProvidersService.getTrialProviders(); } + + async cleanupProviderDeployments(options: ProviderCleanupParams) { + return await this.providerCleanupService.cleanup(options); + } } diff --git a/apps/api/src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer.ts b/apps/api/src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer.ts new file mode 100644 index 000000000..a2e82a84e --- /dev/null +++ b/apps/api/src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer.ts @@ -0,0 +1,25 @@ +interface ProviderCleanupSummary { + deploymentCount: number; +} + +export class ProviderCleanupSummarizer { + private deploymentCount = 0; + + inc(param: keyof ProviderCleanupSummary, value = 1) { + this[param] += value; + } + + set(param: keyof ProviderCleanupSummary, value: number) { + this[param] = value; + } + + get(param: keyof ProviderCleanupSummary) { + return this[param]; + } + + summarize(): ProviderCleanupSummary { + return { + deploymentCount: this.deploymentCount + }; + } +} diff --git a/apps/api/src/deployment/repositories/deployment/deployment.repository.ts b/apps/api/src/deployment/repositories/deployment/deployment.repository.ts index db10f5c13..373eb1fc2 100644 --- a/apps/api/src/deployment/repositories/deployment/deployment.repository.ts +++ b/apps/api/src/deployment/repositories/deployment/deployment.repository.ts @@ -7,6 +7,11 @@ export interface StaleDeploymentsOptions { owner: string; } +export interface ProviderCleanupOptions { + owner: string; + provider: string; +} + export interface StaleDeploymentsOutput { dseq: number; } @@ -37,4 +42,27 @@ export class DeploymentRepository { return deployments ? (deployments as unknown as StaleDeploymentsOutput[]) : []; } + + async findDeploymentsForProvider(options: ProviderCleanupOptions): Promise { + const deployments = await Deployment.findAll({ + attributes: ["dseq"], + where: { + owner: options.owner, + closedHeight: null + }, + include: [ + { + model: Lease, + attributes: [], + required: true, + where: { + providerAddress: options.provider + } + } + ], + raw: true + }); + + return deployments ? (deployments as unknown as StaleDeploymentsOutput[]) : []; + } } diff --git a/apps/api/src/routes/v1/templates/byId.ts b/apps/api/src/routes/v1/templates/byId.ts index 79afe58a5..6fdb2c8c4 100644 --- a/apps/api/src/routes/v1/templates/byId.ts +++ b/apps/api/src/routes/v1/templates/byId.ts @@ -20,18 +20,20 @@ const route = createRoute({ content: { "application/json": { schema: z.object({ - id: z.string(), - name: z.string(), - path: z.string(), - logoUrl: z.string().nullable(), - summary: z.string(), - readme: z.string().nullable(), - deploy: z.string(), - persistentStorageEnabled: z.boolean(), - guide: z.string().nullable(), - githubUrl: z.string(), - config: z.object({ - ssh: z.boolean().optional() + data: z.object({ + id: z.string(), + name: z.string(), + path: z.string(), + logoUrl: z.string().nullable(), + summary: z.string(), + readme: z.string().nullable(), + deploy: z.string(), + persistentStorageEnabled: z.boolean(), + guide: z.string().nullable(), + githubUrl: z.string(), + config: z.object({ + ssh: z.boolean().optional() + }) }) }) } @@ -51,5 +53,5 @@ export default new OpenAPIHono().openapi(route, async c => { return c.text("Template not found", 404); } - return c.json(template); + return c.json({ data: template }); }); diff --git a/apps/api/test/seeders/deployment-grant.seeder.ts b/apps/api/test/seeders/deployment-grant.seeder.ts index 5ef4914c1..f99dd2f15 100644 --- a/apps/api/test/seeders/deployment-grant.seeder.ts +++ b/apps/api/test/seeders/deployment-grant.seeder.ts @@ -18,9 +18,9 @@ export class DeploymentGrantSeeder { spend_limit: { denom: DenomSeeder.create(), amount: faker.number.int({ min: 0, max: 10000000 }).toString() - }, - expiration: faker.date.future().toISOString() - } + } + }, + expiration: faker.date.future().toISOString() }, input ); diff --git a/apps/deploy-web/CHANGELOG.md b/apps/deploy-web/CHANGELOG.md index 6a13ac7e5..2b1a77a77 100644 --- a/apps/deploy-web/CHANGELOG.md +++ b/apps/deploy-web/CHANGELOG.md @@ -1,5 +1,33 @@ +## [2.25.1-beta.1](https://github.com/akash-network/console/compare/console-web/v2.25.1-beta.0...console-web/v2.25.1-beta.1) (2024-12-03) + + +### Bug Fixes + +* ensure proper schema type on server props getter ([aedf03d](https://github.com/akash-network/console/commit/aedf03d837fd6b2ebd6e76b32a694e043053a441)) + +## [2.25.1-beta.0](https://github.com/akash-network/console/compare/console-web/v2.25.0...console-web/v2.25.1-beta.0) (2024-11-28) + + +### Bug Fixes + +* **observability:** ensure pino-pretty works in built app ([7f6f9ca](https://github.com/akash-network/console/commit/7f6f9ca7ca4e1ff4bc3b85735270f61cc8120242)), closes [#474](https://github.com/akash-network/console/issues/474) + +## [2.25.0](https://github.com/akash-network/console/compare/console-web/v2.25.0-beta.1...console-web/v2.25.0) (2024-11-26) + +## [2.25.0-beta.1](https://github.com/akash-network/console/compare/console-web/v2.25.0-beta.0...console-web/v2.25.0-beta.1) (2024-11-26) + + +### Features + +* **deployment:** implement ato top up setting ([1301314](https://github.com/akash-network/console/commit/130131485a68f699587415f96283e0dc83072502)), closes [#412](https://github.com/akash-network/console/issues/412) + + +### Bug Fixes + +* **billing:** ensure checkout pricing is displayed correctly ([3bcb4a8](https://github.com/akash-network/console/commit/3bcb4a881e3bb58e741de8bb8a0a661dede0d8ae)) + ## [2.25.0-beta.0](https://github.com/akash-network/console/compare/console-web/v2.24.1...console-web/v2.25.0-beta.0) (2024-11-23) diff --git a/apps/deploy-web/env/.env b/apps/deploy-web/env/.env index 9a707a612..c83a5625b 100644 --- a/apps/deploy-web/env/.env +++ b/apps/deploy-web/env/.env @@ -10,4 +10,6 @@ NEXT_PUBLIC_REDIRECT_URI='https://console.akash.network/new-deployment' NEXT_PUBLIC_GITHUB_APP_INSTALLATION_URL='https://github.com/apps/akash-console-build-and-deploy-app/installations/new' NEXT_PUBLIC_BITBUCKET_CLIENT_ID=tdH2xfRkTcdqVP6cwW NEXT_PUBLIC_GITHUB_CLIENT_ID=Iv23lidSwihrsSL7aGew -NEXT_PUBLIC_GITLAB_CLIENT_ID=beb5370aad2fdb6147edb44248d20d30c3e189ddfb40c26f651c77bbe949d5a8 \ No newline at end of file +NEXT_PUBLIC_GITLAB_CLIENT_ID=beb5370aad2fdb6147edb44248d20d30c3e189ddfb40c26f651c77bbe949d5a8 + +NEXT_PUBLIC_CI_CD_IMAGE_NAME=hoomanhq/automation \ No newline at end of file diff --git a/apps/deploy-web/mvm.lock b/apps/deploy-web/mvm.lock index 6ed210e74..8b020adc1 100644 --- a/apps/deploy-web/mvm.lock +++ b/apps/deploy-web/mvm.lock @@ -2,6 +2,7 @@ "dependencies": { "@akashnetwork/env-loader": "1.0.1", "@akashnetwork/http-sdk": "1.0.8", + "@akashnetwork/logging": "2.0.2", "@akashnetwork/network-store": "1.0.1", "@akashnetwork/ui": "1.0.0" }, diff --git a/apps/deploy-web/next-env.d.ts b/apps/deploy-web/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/apps/deploy-web/next-env.d.ts +++ b/apps/deploy-web/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index b3d4b7da1..3784e04d9 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-web", - "version": "2.25.0-beta.0", + "version": "2.25.1-beta.1", "private": true, "description": "Web UI to deploy on the Akash Network and view statistic about network usage.", "license": "Apache-2.0", diff --git a/apps/deploy-web/src/components/authorizations/Authorizations.tsx b/apps/deploy-web/src/components/authorizations/Authorizations.tsx index 3fcaf1f35..c4f9a161e 100644 --- a/apps/deploy-web/src/components/authorizations/Authorizations.tsx +++ b/apps/deploy-web/src/components/authorizations/Authorizations.tsx @@ -5,6 +5,7 @@ import { Bank } from "iconoir-react"; import { NextSeo } from "next-seo"; import { Fieldset } from "@src/components/shared/Fieldset"; +import { browserEnvConfig } from "@src/config/browser-env.config"; import { useWallet } from "@src/context/WalletProvider"; import { useAllowance } from "@src/hooks/useAllowance"; import { useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; @@ -26,6 +27,14 @@ type RefreshingType = "granterGrants" | "granteeGrants" | "allowancesIssued" | " const defaultRefetchInterval = 30 * 1000; const refreshingInterval = 1000; +const MASTER_WALLETS = new Set([ + browserEnvConfig.NEXT_PUBLIC_USDC_TOP_UP_MASTER_WALLET_ADDRESS, + browserEnvConfig.NEXT_PUBLIC_UAKT_TOP_UP_MASTER_WALLET_ADDRESS +]); + +const selectNonMaster = (records: Pick[] | Pick[]) => + records.filter(({ grantee }) => !MASTER_WALLETS.has(grantee)); + export const Authorizations: React.FunctionComponent = () => { const { address, signAndBroadcastTx, isManaged } = useWallet(); const { @@ -41,13 +50,15 @@ export const Authorizations: React.FunctionComponent = () => { const [selectedGrants, setSelectedGrants] = useState([]); const [selectedAllowances, setSelectedAllowances] = useState([]); const { data: granterGrants, isLoading: isLoadingGranterGrants } = useGranterGrants(address, { - refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval + refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval, + select: selectNonMaster }); const { data: granteeGrants, isLoading: isLoadingGranteeGrants } = useGranteeGrants(address, { refetchInterval: isRefreshing === "granteeGrants" ? refreshingInterval : defaultRefetchInterval }); const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued } = useAllowancesIssued(address, { - refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval + refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval, + select: selectNonMaster }); useEffect(() => { diff --git a/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx b/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx index cd7ca3ec9..d0971795b 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx @@ -1,6 +1,6 @@ "use client"; -import { createRef, useEffect, useState } from "react"; +import { createRef, FC, useEffect, useState } from "react"; import { Alert, Button, buttonVariants, Spinner, Tabs, TabsList, TabsTrigger } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; import { ArrowLeft } from "iconoir-react"; @@ -9,15 +9,13 @@ import { useRouter, useSearchParams } from "next/navigation"; import { NextSeo } from "next-seo"; import { event } from "nextjs-google-analytics"; -import { CI_CD_TEMPLATE_ID } from "@src/config/remote-deploy.config"; import { useCertificate } from "@src/context/CertificateProvider"; import { useSettings } from "@src/context/SettingsProvider"; -import { useTemplates } from "@src/context/TemplatesProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useDeploymentDetail } from "@src/queries/useDeploymentQuery"; import { useDeploymentLeaseList } from "@src/queries/useLeaseQuery"; import { useProviderList } from "@src/queries/useProvidersQuery"; -import { extractRepositoryUrl, isImageInYaml } from "@src/services/remote-deploy/remote-deployment-controller.service"; +import { extractRepositoryUrl, isCiCdImageInYaml } from "@src/services/remote-deploy/remote-deployment-controller.service"; import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { RouteStep } from "@src/types/route-steps.type"; import { getDeploymentLocalData } from "@src/utils/deploymentLocalDataUtils"; @@ -31,7 +29,11 @@ import { DeploymentSubHeader } from "./DeploymentSubHeader"; import { LeaseRow } from "./LeaseRow"; import { ManifestUpdate } from "./ManifestUpdate"; -export function DeploymentDetail({ dseq }: React.PropsWithChildren<{ dseq: string }>) { +export interface DeploymentDetailProps { + dseq: string; +} + +export const DeploymentDetail: FC = ({ dseq }) => { const router = useRouter(); const [activeTab, setActiveTab] = useState("LEASES"); const [editedManifest, setEditedManifest] = useState(null); @@ -39,9 +41,7 @@ export function DeploymentDetail({ dseq }: React.PropsWithChildren<{ dseq: strin const { isSettingsInit } = useSettings(); const [leaseRefs, setLeaseRefs] = useState>([]); const [deploymentManifest, setDeploymentManifest] = useState(null); - const { getTemplateById } = useTemplates(); - const remoteDeployTemplate = getTemplateById(CI_CD_TEMPLATE_ID); - const isRemoteDeploy: boolean = !!editedManifest && !!isImageInYaml(editedManifest, remoteDeployTemplate?.deploy); + const isRemoteDeploy: boolean = !!editedManifest && !!isCiCdImageInYaml(editedManifest); const repo: string | null = isRemoteDeploy ? extractRepositoryUrl(editedManifest) : null; const { @@ -255,4 +255,4 @@ export function DeploymentDetail({ dseq }: React.PropsWithChildren<{ dseq: strin )} ); -} +}; diff --git a/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx b/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx index e264fc36a..97a8f9899 100644 --- a/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx +++ b/apps/deploy-web/src/components/get-started/GetStartedStepper.tsx @@ -104,7 +104,7 @@ export const GetStartedStepper: React.FunctionComponent = () => {
{isManagedWallet && ( - + = ({ isLoadingBalances, )} {isManagedWallet && ( - + { +export interface NewDeploymentContainerProps { + template?: TemplateOutput; + templateId?: string; +} + +export const NewDeploymentContainer: FC = ({ template: requestedTemplate, templateId }) => { const [isGitProviderTemplate, setIsGitProviderTemplate] = useState(false); const { isLoading: isLoadingTemplates, templates } = useTemplates(); const [activeStep, setActiveStep] = useState(null); @@ -28,7 +34,6 @@ export const NewDeploymentContainer: FC = () => { const [editedManifest, setEditedManifest] = useState(null); const deploySdl = useAtomValue(sdlStore.deploySdl); const { getDeploymentData } = useLocalNotes(); - const { getTemplateById } = useTemplates(); const router = useRouter(); const searchParams = useSearchParams(); const dseq = searchParams?.get("dseq"); @@ -78,8 +83,7 @@ export const NewDeploymentContainer: FC = () => { toggleCmp("ssh"); } - const cicdTemplate = getTemplateById(CI_CD_TEMPLATE_ID); - const isRemoteYamlImage = isImageInYaml(template?.content as string, cicdTemplate?.deploy); + const isRemoteYamlImage = isCiCdImageInYaml(template?.content as string); const queryStep = searchParams?.get("step"); if (queryStep !== RouteStep.editDeployment) { if (isRemoteYamlImage) { @@ -113,34 +117,25 @@ export const NewDeploymentContainer: FC = () => { return template; }; - const getGalleryTemplate = (): Partial<{ - code: string; - name: string; - content: string; - valuesToChange: any[]; - config: { ssh?: boolean }; - }> | null => { - const queryTemplateId = searchParams?.get("templateId"); - if (queryTemplateId) { - const templateById = getTemplateById(queryTemplateId as string); - if (templateById) { - return { + const getGalleryTemplate = useCallback((): + | Partial<{ + code: string; + name: string; + content: string; + valuesToChange: any[]; + config: { ssh?: boolean }; + }> + | undefined => { + return requestedTemplate + ? { code: "empty", - name: templateById.name, - content: templateById.deploy, - valuesToChange: templateById.valuesToChange || [], - config: templateById.config - }; - } - - const hardCodedTemplate = hardcodedTemplates.find(t => t.code === queryTemplateId); - if (hardCodedTemplate) { - return hardCodedTemplate; - } - } - - return null; - }; + name: requestedTemplate.name, + content: requestedTemplate.deploy, + valuesToChange: [], + config: requestedTemplate.config + } + : hardcodedTemplates.find(t => t.code === templateId); + }, [requestedTemplate, templateId]); function getStepIndexByParam(step: (typeof RouteStep)[keyof typeof RouteStep] | null) { switch (step) { diff --git a/apps/deploy-web/src/components/remote-deploy/update/RemoteDeployUpdate.tsx b/apps/deploy-web/src/components/remote-deploy/update/RemoteDeployUpdate.tsx index f1d8c600c..ab242e426 100644 --- a/apps/deploy-web/src/components/remote-deploy/update/RemoteDeployUpdate.tsx +++ b/apps/deploy-web/src/components/remote-deploy/update/RemoteDeployUpdate.tsx @@ -6,9 +6,9 @@ import { useSnackbar } from "notistack"; import { EnvFormModal } from "@src/components/sdl/EnvFormModal"; import { EnvVarList } from "@src/components/sdl/EnvVarList"; -import { CI_CD_TEMPLATE_ID, CURRENT_SERVICE, protectedEnvironmentVariables } from "@src/config/remote-deploy.config"; +import { browserEnvConfig } from "@src/config/browser-env.config"; +import { CURRENT_SERVICE, protectedEnvironmentVariables } from "@src/config/remote-deploy.config"; import { SdlBuilderProvider } from "@src/context/SdlBuilderProvider"; -import { useTemplates } from "@src/context/TemplatesProvider"; import { EnvVarUpdater } from "@src/services/remote-deploy/remote-deployment-controller.service"; import { tokens } from "@src/store/remoteDeployStore"; import { SdlBuilderFormValuesType, ServiceType } from "@src/types"; @@ -26,8 +26,6 @@ const RemoteDeployUpdate = ({ sdlString, onManifestChange }: { sdlString: string const [isEditingEnv, setIsEditingEnv] = useState(false); const { control, watch, setValue } = useForm({ defaultValues: { services: [defaultService] } }); const { fields: services } = useFieldArray({ control, name: "services", keyName: "id" }); - const { getTemplateById } = useTemplates(); - const remoteDeployTemplate = getTemplateById(CI_CD_TEMPLATE_ID); const envVarUpdater = useMemo(() => new EnvVarUpdater(services), [services]); useEffect(() => { @@ -51,10 +49,7 @@ const RemoteDeployUpdate = ({ sdlString, onManifestChange }: { sdlString: string const createAndValidateSdl = (yamlStr: string) => { try { - if (!yamlStr) return []; - const services = importSimpleSdl(yamlStr); - - return services; + return yamlStr ? importSimpleSdl(yamlStr) : []; } catch (err) { if (err.name === "YAMLException" || err.name === "CustomValidationError") { enqueueSnackbar(, { variant: "error" }); @@ -65,7 +60,7 @@ const RemoteDeployUpdate = ({ sdlString, onManifestChange }: { sdlString: string } } }; - return remoteDeployTemplate?.deploy?.includes(services?.[0]?.image) && services?.[0]?.env && services?.[0]?.env?.length > 0 ? ( + return services?.[0]?.image.startsWith(browserEnvConfig.NEXT_PUBLIC_CI_CD_IMAGE_NAME) && services?.[0]?.env && services?.[0]?.env?.length > 0 ? (
diff --git a/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSetting.tsx b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSetting.tsx new file mode 100644 index 000000000..5839f8bf1 --- /dev/null +++ b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSetting.tsx @@ -0,0 +1,218 @@ +import React, { FC, useCallback, useEffect, useMemo } from "react"; +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +import { Button, Form, FormField, FormInput } from "@akashnetwork/ui/components"; +import { zodResolver } from "@hookform/resolvers/zod"; +import addYears from "date-fns/addYears"; +import format from "date-fns/format"; +import { z } from "zod"; + +import { aktToUakt, uaktToAKT } from "@src/utils/priceUtils"; + +const positiveNumberSchema = z.coerce.number().min(0, { + message: "Amount must be greater or equal to 0." +}); + +const formSchema = z + .object({ + uaktFeeLimit: positiveNumberSchema, + usdcFeeLimit: positiveNumberSchema, + uaktDeploymentLimit: positiveNumberSchema, + usdcDeploymentLimit: positiveNumberSchema, + expiration: z.string().min(1, "Expiration is required.") + }) + .refine( + data => { + if (data.usdcDeploymentLimit > 0) { + return data.usdcFeeLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `USDC Deployments Limit` is greater than 0", + path: ["usdcFeeLimit"] + } + ) + .refine( + data => { + if (data.usdcFeeLimit > 0) { + return data.usdcDeploymentLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `USDC Fees Limit` is greater than 0", + path: ["usdcDeploymentLimit"] + } + ) + .refine( + data => { + if (data.uaktDeploymentLimit > 0) { + return data.uaktFeeLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `AKT Deployments Limit` is greater than 0", + path: ["uaktFeeLimit"] + } + ) + .refine( + data => { + if (data.uaktFeeLimit > 0) { + return data.uaktDeploymentLimit > 0; + } + return true; + }, + { + message: "Must be greater than 0 if `AKT Fees Limit` is greater than 0", + path: ["uaktDeploymentLimit"] + } + ); + +type FormValues = z.infer; + +type LimitFields = keyof Omit; + +type AutoTopUpSubmitHandler = (action: "revoke-all" | "update", next: FormValues) => Promise; + +export interface AutoTopUpSettingProps extends Partial> { + onSubmit: AutoTopUpSubmitHandler; + expiration?: Date; +} + +const fields: LimitFields[] = ["uaktFeeLimit", "usdcFeeLimit", "uaktDeploymentLimit", "usdcDeploymentLimit"]; + +export const AutoTopUpSetting: FC = ({ onSubmit, expiration, ...props }) => { + const hasAny = useMemo(() => fields.some(field => props[field]), [props]); + + const defaultLimitValues = useMemo(() => { + return fields.reduce( + (acc, field) => { + acc[field] = uaktToAKT(props[field] || 0); + return acc; + }, + {} as Record + ); + }, [props]); + + const form = useForm>({ + defaultValues: { + ...defaultLimitValues, + expiration: format(expiration || addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm") + }, + resolver: zodResolver(formSchema) + }); + const { handleSubmit, control, setValue, reset } = form; + + useEffect(() => { + setValue("uaktFeeLimit", uaktToAKT(props.uaktFeeLimit || 0)); + }, [props.uaktFeeLimit]); + + useEffect(() => { + setValue("usdcFeeLimit", uaktToAKT(props.usdcFeeLimit || 0)); + }, [props.usdcFeeLimit]); + + useEffect(() => { + setValue("uaktDeploymentLimit", uaktToAKT(props.uaktDeploymentLimit || 0)); + }, [props.uaktDeploymentLimit]); + + useEffect(() => { + setValue("usdcDeploymentLimit", uaktToAKT(props.usdcDeploymentLimit || 0)); + }, [props.usdcDeploymentLimit]); + + useEffect(() => { + if (expiration) { + setValue("expiration", format(expiration || addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm")); + } + }, [expiration]); + + const execSubmitterRoleAction: SubmitHandler = useCallback( + async (next: FormValues, event: React.BaseSyntheticEvent) => { + const role = event.nativeEvent.submitter?.getAttribute("data-role"); + await onSubmit(role as "revoke-all" | "update", convertToUakt(next)); + reset(next); + }, + [onSubmit, reset] + ); + + return ( +
+
+ +
Deployments billed in AKT
+
+
+ { + return ; + }} + /> +
+ +
+ { + return ; + }} + /> +
+
+ +
Deployments billed in USDC
+
+
+ { + return ; + }} + /> +
+ +
+ { + return ; + }} + /> +
+
+ +
+ { + return ; + }} + /> +
+ + + + {hasAny && ( + + )} +
+ +
+ ); +}; + +function convertToUakt({ ...values }: FormValues) { + return fields.reduce((acc, field) => { + acc[field] = aktToUakt(values[field]); + return acc; + }, values); +} diff --git a/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx new file mode 100644 index 000000000..5eb6e0e2b --- /dev/null +++ b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx @@ -0,0 +1,52 @@ +import React, { FC, useCallback, useEffect } from "react"; + +import { AutoTopUpSetting, AutoTopUpSettingProps } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSetting"; +import { useWallet } from "@src/context/WalletProvider"; +import { useAutoTopUpLimits } from "@src/hooks/useAutoTopUpLimits"; +import { useAutoTopUpService } from "@src/hooks/useAutoTopUpService"; + +export const AutoTopUpSettingContainer: FC = () => { + const { address, signAndBroadcastTx } = useWallet(); + const { fetch, uaktFeeLimit, usdcFeeLimit, uaktDeploymentLimit, usdcDeploymentLimit, expiration } = useAutoTopUpLimits(); + const autoTopUpMessageService = useAutoTopUpService(); + + useEffect(() => { + fetch(); + }, []); + + const updateAllowancesAndGrants: AutoTopUpSettingProps["onSubmit"] = useCallback( + async (action, next) => { + const prev = { + uaktFeeLimit, + usdcFeeLimit, + uaktDeploymentLimit, + usdcDeploymentLimit, + expiration + }; + + const messages = autoTopUpMessageService.collectMessages({ + granter: address, + prev, + next: action === "revoke-all" ? undefined : { ...next, expiration: new Date(next.expiration) } + }); + + if (messages.length) { + await signAndBroadcastTx(messages); + } + + await fetch(); + }, + [address, autoTopUpMessageService, expiration, fetch, signAndBroadcastTx, uaktDeploymentLimit, uaktFeeLimit, usdcDeploymentLimit, usdcFeeLimit] + ); + + return ( + + ); +}; diff --git a/apps/deploy-web/src/components/settings/SettingsContainer.tsx b/apps/deploy-web/src/components/settings/SettingsContainer.tsx index 560cdc990..d6303e513 100644 --- a/apps/deploy-web/src/components/settings/SettingsContainer.tsx +++ b/apps/deploy-web/src/components/settings/SettingsContainer.tsx @@ -5,6 +5,8 @@ import { Edit } from "iconoir-react"; import { useRouter } from "next/navigation"; import { NextSeo } from "next-seo"; +import { AutoTopUpSetting } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSetting"; +import { AutoTopUpSettingContainer } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer"; import { LocalDataManager } from "@src/components/settings/LocalDataManager"; import { Fieldset } from "@src/components/shared/Fieldset"; import { LabelValue } from "@src/components/shared/LabelValue"; @@ -58,6 +60,10 @@ export const SettingsContainer: React.FunctionComponent = () => { + +
+ +
diff --git a/apps/deploy-web/src/components/settings/SettingsForm.tsx b/apps/deploy-web/src/components/settings/SettingsForm.tsx index b439e472d..c40fcb96a 100644 --- a/apps/deploy-web/src/components/settings/SettingsForm.tsx +++ b/apps/deploy-web/src/components/settings/SettingsForm.tsx @@ -94,9 +94,7 @@ export const SettingsForm: React.FunctionComponent = () => { control={control} name="apiEndpoint" defaultValue={settings.apiEndpoint} - render={({ field }) => { - return ; - }} + render={({ field }) => } /> ) : (

{settings.apiEndpoint}

@@ -111,9 +109,7 @@ export const SettingsForm: React.FunctionComponent = () => { control={control} name="rpcEndpoint" defaultValue={settings.rpcEndpoint} - render={({ field }) => { - return ; - }} + render={({ field }) => } /> ) : (

{settings.rpcEndpoint}

diff --git a/apps/deploy-web/src/components/templates/TemplateDetail.tsx b/apps/deploy-web/src/components/templates/TemplateDetail.tsx index d38ab6e9d..85e654424 100644 --- a/apps/deploy-web/src/components/templates/TemplateDetail.tsx +++ b/apps/deploy-web/src/components/templates/TemplateDetail.tsx @@ -1,5 +1,6 @@ "use client"; -import { useState } from "react"; + +import { FC, useCallback, useState } from "react"; import { Button, buttonVariants, Tabs, TabsList, TabsTrigger } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; import GitHubIcon from "@mui/icons-material/GitHub"; @@ -8,10 +9,8 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { DynamicMonacoEditor } from "@src/components/shared/DynamicMonacoEditor"; -import { LinearLoadingSkeleton } from "@src/components/shared/LinearLoadingSkeleton"; import Markdown from "@src/components/shared/Markdown"; import ViewPanel from "@src/components/shared/ViewPanel"; -import { useTemplates } from "@src/context/TemplatesProvider"; import { usePreviousRoute } from "@src/hooks/usePreviousRoute"; import { getShortText } from "@src/hooks/useShortText"; import { ApiTemplate } from "@src/types"; @@ -20,37 +19,35 @@ import { domainName, UrlService } from "@src/utils/urlUtils"; import Layout from "../layout/Layout"; import { CustomNextSeo } from "../shared/CustomNextSeo"; -type Props = { +export interface TemplateDetailProps { templateId: string; template: ApiTemplate; -}; +} -export const TemplateDetail: React.FunctionComponent = ({ templateId, template }) => { +export const TemplateDetail: FC = ({ templateId, template }) => { const [activeTab, setActiveTab] = useState("README"); - const { getTemplateById, isLoading } = useTemplates(); const router = useRouter(); - const _template = template || getTemplateById(templateId); const previousRoute = usePreviousRoute(); - function handleBackClick() { + const goBack = useCallback(() => { if (previousRoute) { router.back(); } else { router.push(UrlService.templates()); } - } + }, [previousRoute, router]); - function handleOpenGithub() { - window.open(_template.githubUrl, "_blank"); - } + const openGithub = useCallback(() => { + window.open(template.githubUrl, "_blank"); + }, [template]); return (
@@ -61,32 +58,31 @@ export const TemplateDetail: React.FunctionComponent = ({ templateId, tem View SDL - {_template?.guide && ( + {template.guide && ( Guide )} -
-
-

{_template?.name}

+

{template.name}

-
Deploy  @@ -97,21 +93,21 @@ export const TemplateDetail: React.FunctionComponent = ({ templateId, tem {activeTab === "README" && (
- {_template?.readme} + {template.readme}
)} {activeTab === "SDL" && (
- +
)} {activeTab === "GUIDE" && (
- {_template?.guide} + {template.guide}
)} diff --git a/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx b/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx index 66abd64ff..8932fe92a 100644 --- a/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx +++ b/apps/deploy-web/src/components/top-up-amount-picker/TopUpAmountPicker.tsx @@ -1,6 +1,8 @@ import React from "react"; import { Button, buttonVariants } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; +import { useMediaQuery } from "@mui/material"; +import { useTheme as useMuiTheme } from "@mui/material/styles"; import { MoreHoriz } from "iconoir-react"; import { LoginRequiredLink } from "@src/components/user/LoginRequiredLink"; @@ -10,43 +12,55 @@ import { FCWithChildren } from "@src/types/component"; interface TopUpAmountPickerProps { popoverClassName?: string; - fullWidth?: boolean; + className?: string; + mdMode: "hover" | "click"; } -export const TopUpAmountPicker: FCWithChildren = ({ children, popoverClassName, fullWidth }) => { +export const TopUpAmountPicker: FCWithChildren = ({ children, popoverClassName, className, mdMode }) => { const user = useUser(); + const [isOpened, setIsOpened] = React.useState(false); const { data = [] } = useStripePricesQuery({ enabled: !!user?.userId }); + const muiTheme = useMuiTheme(); + const isSmallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); + const isOnClick = mdMode === "click" || isSmallScreen; - return data.length > 1 ? ( - - - {children} - - -
- {data.map(price => { - return ( - - {price.isCustom ? "Custom" : "$"} - {price.unitAmount} - - ); - })} -
-
- ) : ( - <>{children} + return ( +
+ {data.length > 1 ? ( + <> +
+ {children} + {isOnClick && ( + + )} +
+
+ {data.map(price => { + return ( + + {price.isCustom ? "Custom" : "$"} + {price.unitAmount} + + ); + })} +
+ + ) : ( + <>{children} + )} +
); }; diff --git a/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx b/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx index 8cd24fcac..71fc6d259 100644 --- a/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx +++ b/apps/deploy-web/src/components/wallet/ManagedWalletPopup.tsx @@ -71,7 +71,7 @@ export const ManagedWalletPopup: React.FC = ({ walletBa )}
- + +
+ {showSuccess && ( + + Success + Provider attributes updated successfully + + )}
); diff --git a/apps/provider-console/src/components/become-provider/ServerForm.tsx b/apps/provider-console/src/components/become-provider/ServerForm.tsx index 951ee7f09..a20da9747 100644 --- a/apps/provider-console/src/components/become-provider/ServerForm.tsx +++ b/apps/provider-console/src/components/become-provider/ServerForm.tsx @@ -26,7 +26,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useAtom } from "jotai/react"; import { z } from "zod"; +import { useControlMachine } from "@src/context/ControlMachineProvider"; +import { useWallet } from "@src/context/WalletProvider"; import providerProcessStore from "@src/store/providerProcessStore"; +import { ControlMachineWithAddress } from "@src/types/controlMachine"; import restClient from "@src/utils/restClient"; import { ResetProviderForm } from "./ResetProviderProcess"; @@ -55,12 +58,16 @@ type AccountFormValues = z.infer; interface ServerFormProp { currentServerNumber: number; onComplete: () => void; + editMode?: boolean; + controlMachine?: ControlMachineWithAddress | null; } -export const ServerForm: React.FC = ({ currentServerNumber, onComplete }) => { +export const ServerForm: React.FC = ({ currentServerNumber, onComplete, editMode = false, controlMachine }) => { const [providerProcess, setProviderProcess] = useAtom(providerProcessStore.providerProcessAtom); const [selectedFile, setSelectedFile] = useState(null); const [storedFileContent, setStoredFileContent] = useState(null); + const { setControlMachine } = useControlMachine(); + const { address } = useWallet(); const getDefaultValues = () => { if (currentServerNumber === 0 || !providerProcess?.storeInformation) { @@ -84,18 +91,18 @@ export const ServerForm: React.FC = ({ currentServerNumber, onCo const form = useForm({ resolver: zodResolver(accountFormSchema), - defaultValues: getDefaultValues() as any + defaultValues: editMode ? controlMachine?.access : (getDefaultValues() as any) }); useEffect(() => { if (currentServerNumber > 0 && providerProcess?.storeInformation) { - const firstServer = providerProcess.machines[0]?.access; - if (firstServer.file) { + const firstServer = editMode ? controlMachine?.access : providerProcess.machines[0]?.access; + if (firstServer?.file) { setStoredFileContent(typeof firstServer.file === "string" ? firstServer.file : null); form.setValue("authType", "file"); } } - }, [currentServerNumber, providerProcess, form]); + }, [currentServerNumber, providerProcess, form, editMode, controlMachine]); const [verificationError, setVerificationError] = useState<{ message: string; details: string[] } | null>(null); const [, setVerificationResult] = useState(null); @@ -127,7 +134,7 @@ export const ServerForm: React.FC = ({ currentServerNumber, onCo } let response: any; - if (currentServerNumber === 0) { + if (currentServerNumber === 0 || editMode) { response = await restClient.post("/verify/control-machine", jsonData, { headers: { "Content-Type": "application/json" } }); @@ -152,21 +159,29 @@ export const ServerForm: React.FC = ({ currentServerNumber, onCo } if (response.status === "success") { - const machines = [...(providerProcess?.machines ?? [])]; - machines[currentServerNumber] = { + const machine = { access: { ...formValues, file: formValues.file && formValues.file[0] ? await readFileAsBase64(formValues.file[0]) : storedFileContent }, systemInfo: response.data.system_info }; + if (!editMode) { + const machines = [...(providerProcess?.machines ?? [])]; + machines[currentServerNumber] = machine; - setProviderProcess({ - ...providerProcess, - machines, - storeInformation: currentServerNumber === 0 ? formValues.saveInformation : providerProcess?.storeInformation, - process: providerProcess.process - }); + setProviderProcess({ + ...providerProcess, + machines, + storeInformation: currentServerNumber === 0 ? formValues.saveInformation : providerProcess?.storeInformation, + process: providerProcess.process + }); + } else { + setControlMachine({ + address, + ...machine + }); + } onComplete(); } } catch (error: any) { @@ -206,10 +221,9 @@ export const ServerForm: React.FC = ({ currentServerNumber, onCo

- {currentServerNumber === 0 && "Control Plane Machine Access"} - {currentServerNumber !== 0 && "Node Access"} + {editMode ? "Control Machine Access" : currentServerNumber === 0 ? "Control Plane Machine Access" : "Node Access"}

-

Enter the required details for your control plane setup

+

Enter the required details for your {editMode ? "control machine" : "control plane setup"}

@@ -345,9 +359,7 @@ export const ServerForm: React.FC = ({ currentServerNumber, onCo
-
- -
+
{!editMode && }
- {currentServerNumber === 0 && ( + {currentServerNumber === 0 && !editMode && (

Heads up!

diff --git a/apps/provider-console/src/components/become-provider/WalletImport.tsx b/apps/provider-console/src/components/become-provider/WalletImport.tsx index c6d78f2ef..96d72b418 100644 --- a/apps/provider-console/src/components/become-provider/WalletImport.tsx +++ b/apps/provider-console/src/components/become-provider/WalletImport.tsx @@ -21,7 +21,10 @@ import { useAtom } from "jotai"; import { useRouter } from "next/router"; import { z } from "zod"; +import { useControlMachine } from "@src/context/ControlMachineProvider"; +import { useWallet } from "@src/context/WalletProvider"; import providerProcessStore from "@src/store/providerProcessStore"; +import { ControlMachineWithAddress } from "@src/types/controlMachine"; import restClient from "@src/utils/restClient"; import { ResetProviderForm } from "./ResetProviderProcess"; @@ -77,6 +80,8 @@ export const WalletImport: React.FC = ({ onComplete }) => { const [providerProcess] = useAtom(providerProcessStore.providerProcessAtom); const [, resetProviderProcess] = useAtom(providerProcessStore.resetProviderProcess); + const { setControlMachine } = useControlMachine(); + const { address } = useWallet(); const defaultValues: Partial = { walletMode: "seed" @@ -95,51 +100,58 @@ export const WalletImport: React.FC = ({ onComplete }) => { }; const submitForm = async (data: SeedFormValues) => { - if (!providerProcess.machines || providerProcess.machines.length === 0) { - setError("No machine information available"); - } setIsLoading(true); setError(null); try { - const publicKey = providerProcess.machines[0].systemInfo.public_key; - const keyId = providerProcess.machines[0].systemInfo.key_id; - const encryptedSeedPhrase = await encrypt(data.seedPhrase, publicKey); + if (providerProcess.machines && providerProcess.machines.length > 0) { + const publicKey = providerProcess.machines[0].systemInfo.public_key; + const keyId = providerProcess.machines[0].systemInfo.key_id; + const encryptedSeedPhrase = await encrypt(data.seedPhrase, publicKey); - const finalRequest = { - wallet: { - key_id: keyId, - wallet_phrase: encryptedSeedPhrase - }, - nodes: providerProcess.machines.map(machine => ({ - hostname: machine.access.hostname, - port: machine.access.port, - username: machine.access.username, - keyfile: machine.access.file, - password: machine.access.password, - install_gpu_drivers: machine.systemInfo.gpu.count > 0 ? true : false - })), - provider: { - attributes: providerProcess.attributes, - pricing: providerProcess.pricing, - config: providerProcess.config - } - }; + const finalRequest = { + wallet: { + key_id: keyId, + wallet_phrase: encryptedSeedPhrase + }, + nodes: providerProcess.machines.map(machine => ({ + hostname: machine.access.hostname, + port: machine.access.port, + username: machine.access.username, + keyfile: machine.access.file, + password: machine.access.password, + install_gpu_drivers: machine.systemInfo.gpu.count > 0 ? true : false + })), + provider: { + attributes: providerProcess.attributes, + pricing: providerProcess.pricing, + config: providerProcess.config + } + }; - const response: any = await restClient.post("/build-provider", finalRequest, { - headers: { "Content-Type": "application/json" } - }); + const response: any = await restClient.post("/build-provider", finalRequest, { + headers: { "Content-Type": "application/json" } + }); - if (response.action_id) { - resetProviderProcess(); - router.push(`/action?id=${response.action_id}`); + if (response.action_id) { + const machineWithAddress: ControlMachineWithAddress = { + address: address, + ...providerProcess.machines[0] + }; + await setControlMachine(machineWithAddress); + resetProviderProcess(); + router.push(`/actions/${response.action_id}`); + } else { + throw new Error("Invalid response from server"); + } } else { - throw new Error("Invalid response from server"); + throw new Error("No machine information available"); } } catch (error) { + console.error("Error during wallet verification:", error); setError("An error occurred while processing your request. Please try again."); } finally { - onComplete(); setIsLoading(false); + onComplete(); } }; diff --git a/apps/provider-console/src/components/home/HomeContainer.tsx b/apps/provider-console/src/components/home/HomeContainer.tsx index a0aca8862..8f9ba9105 100644 --- a/apps/provider-console/src/components/home/HomeContainer.tsx +++ b/apps/provider-console/src/components/home/HomeContainer.tsx @@ -12,14 +12,10 @@ import { WalletNotConnected } from "./WalletNotConnected"; export const HomeContainer: React.FC = () => { const router = useRouter(); - const { isWalletConnected, isWalletArbitrarySigned, isProvider, isOnline, isProviderStatusFetched } = useWallet(); - const [isLoading, setIsLoading] = useState(false); + const { isWalletConnected, isProvider, isOnline, isProviderStatusFetched } = useWallet(); + const [isLoading] = useState(false); const { data: providerActions } = useProviderActions(); - useEffect(() => { - setIsLoading(true); - }, [isProvider, isOnline, isWalletArbitrarySigned]); - useEffect(() => { if (isWalletConnected && isProvider) { router.push("/dashboard"); diff --git a/apps/provider-console/src/components/layout/Sidebar.tsx b/apps/provider-console/src/components/layout/Sidebar.tsx index 28b1bad51..2035d8b98 100644 --- a/apps/provider-console/src/components/layout/Sidebar.tsx +++ b/apps/provider-console/src/components/layout/Sidebar.tsx @@ -1,6 +1,6 @@ "use client"; import React, { ReactNode, useState } from "react"; -import { Button, buttonVariants } from "@akashnetwork/ui/components"; +import { Button, buttonVariants, Spinner } from "@akashnetwork/ui/components"; import Drawer from "@mui/material/Drawer"; import { useTheme as useMuiTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -10,6 +10,7 @@ import getConfig from "next/config"; import Image from "next/image"; import Link from "next/link"; +import { useControlMachine } from "@src/context/ControlMachineProvider"; import { useWallet } from "@src/context/WalletProvider"; import { ISidebarGroupMenu } from "@src/types"; import { closedDrawerWidth, drawerWidth } from "@src/utils/constants"; @@ -35,6 +36,8 @@ export const Sidebar: React.FC = ({ isMobileOpen, handleDrawerToggle, isN const muiTheme = useMuiTheme(); const smallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); + const { activeControlMachine, openControlMachineDrawer, controlMachineLoading } = useControlMachine(); + const routeGroups: ISidebarGroupMenu[] = [ { hasDivider: false, @@ -54,9 +57,9 @@ export const Sidebar: React.FC = ({ isMobileOpen, handleDrawerToggle, isN { title: "Actions", icon: props => , - url: "#", - activeRoutes: ["#"], - disabled: true + url: UrlService.actions(), + activeRoutes: [UrlService.actions()], + disabled: false }, { title: "Pricing", @@ -68,9 +71,9 @@ export const Sidebar: React.FC = ({ isMobileOpen, handleDrawerToggle, isN { title: "Attributes", icon: props => , - url: "#", - activeRoutes: ["#"], - disabled: true + url: UrlService.attributes(), + activeRoutes: [UrlService.attributes()], + disabled: false }, { title: "Settings", @@ -162,6 +165,37 @@ export const Sidebar: React.FC = ({ isMobileOpen, handleDrawerToggle, isN {_isNavOpen && (
{/* */} + {controlMachineLoading ? ( +
+
+ Machine: +
+ +
Connecting...
+
+
+
+ ) : activeControlMachine ? ( +
+
+ Machine: +
+
+ {activeControlMachine.access.hostname} +
+
+
+ ) : ( +
+
+ Machine: +
+
+
Not Connected
+
+
+
+ )}
= ({ isMobileOpen, handleDrawerToggle, isN return (