From 4c6887a5268c021d0a0832ac9f051cf9ba0313ff Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Wed, 14 Jan 2026 17:58:51 +0100 Subject: [PATCH 1/8] feat(new-nav): basis for Project overview page --- .../header/breadcrumbs/breadcrumbs.tsx | 51 ++++- .../src/app/components/header/header.tsx | 3 + apps/console-v5/src/routeTree.gen.ts | 100 +++++++++ .../environment/$environmentId/index.tsx | 19 ++ .../environment/$environmentId/overview.tsx | 31 +++ .../project/$projectId/index.tsx | 13 ++ .../project/$projectId/overview.tsx | 211 ++++++++++++++++++ .../_authenticated/organization/route.tsx | 67 +++++- .../create-clone-environment-modal.tsx | 11 +- .../hooks/use-environment/use-environment.ts | 4 +- .../use-environments/use-environments.ts | 4 +- .../src/lib/hooks/use-project/use-project.ts | 4 +- .../src/lib/project-list/project-list.tsx | 9 +- .../lib/hooks/use-services/use-services.ts | 4 +- libs/shared/ui/src/index.ts | 1 + .../components/env-type/env-type.stories.tsx | 58 +++++ .../src/lib/components/env-type/env-type.tsx | 43 ++++ .../table-primitives/table-primitives.tsx | 4 +- 18 files changed, 601 insertions(+), 36 deletions(-) create mode 100644 apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/index.tsx create mode 100644 apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview.tsx create mode 100644 apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/index.tsx create mode 100644 apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx create mode 100644 libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx create mode 100644 libs/shared/ui/src/lib/components/env-type/env-type.tsx diff --git a/apps/console-v5/src/app/components/header/breadcrumbs/breadcrumbs.tsx b/apps/console-v5/src/app/components/header/breadcrumbs/breadcrumbs.tsx index 567de536c7b..a9434dcaa87 100644 --- a/apps/console-v5/src/app/components/header/breadcrumbs/breadcrumbs.tsx +++ b/apps/console-v5/src/app/components/header/breadcrumbs/breadcrumbs.tsx @@ -2,32 +2,37 @@ import { useParams, useRouter } from '@tanstack/react-router' import { useMemo } from 'react' import { ClusterAvatar, useClusters } from '@qovery/domains/clusters/feature' import { useOrganization, useOrganizations } from '@qovery/domains/organizations/feature' +import { useProjects } from '@qovery/domains/projects/feature' import { Avatar } from '@qovery/shared/ui' import { Separator } from '../header' import { BreadcrumbItem, type BreadcrumbItemData } from './breadcrumb-item' export function Breadcrumbs() { const { buildLocation } = useRouter() - const { organizationId, clusterId } = useParams({ strict: false }) + const { organizationId, clusterId, projectId } = useParams({ strict: false }) const { data: organizations = [] } = useOrganizations({ enabled: true, + suspense: true, }) - const { data: organization } = useOrganization({ organizationId, enabled: !!organizationId }) - const { data: clusters = [] } = useClusters({ organizationId }) + const { data: organization } = useOrganization({ organizationId, enabled: !!organizationId, suspense: true }) + const { data: clusters = [] } = useClusters({ organizationId, suspense: true }) + const { data: projects = [] } = useProjects({ organizationId, suspense: true }) // Necessary to keep the organization from client by Qovery team const allOrganizations = organizations.find((org) => org.id !== organizationId) && organization - ? [...organizations, organization] + ? [...organizations.filter((org) => org.id !== organizationId), organization] : organizations - const orgItems: BreadcrumbItemData[] = allOrganizations.map((organization) => ({ - id: organization.id, - label: organization.name, - path: buildLocation({ to: '/organization/$organizationId', params: { organizationId: organization.id } }).href, - logo_url: organization.logo_url ?? undefined, - })) + const orgItems: BreadcrumbItemData[] = allOrganizations + .sort((a, b) => a.name.trim().localeCompare(b.name.trim())) + .map((organization) => ({ + id: organization.id, + label: organization.name, + path: buildLocation({ to: '/organization/$organizationId', params: { organizationId: organization.id } }).href, + logo_url: organization.logo_url ?? undefined, + })) const currentOrg = useMemo( () => orgItems.find((organization) => organization.id === organizationId), @@ -43,11 +48,27 @@ export function Breadcrumbs() { }).href, })) + const projectItems: BreadcrumbItemData[] = projects + .sort((a, b) => a.name.trim().localeCompare(b.name.trim())) + .map((project) => ({ + id: project.id, + label: project.name, + path: buildLocation({ + to: '/organization/$organizationId/project/$projectId/overview', + params: { organizationId, projectId: project.id }, + }).href, + })) + const currentCluster = useMemo( () => clusterItems.find((cluster) => cluster.id === clusterId), [clusterId, clusterItems] ) + const currentProject = useMemo( + () => projectItems.find((project) => project.id === projectId), + [projectId, projectItems] + ) + const breadcrumbData: Array<{ item: BreadcrumbItemData; items: BreadcrumbItemData[] }> = [] if (currentOrg) { @@ -78,6 +99,16 @@ export function Breadcrumbs() { }) } + if (currentProject) { + breadcrumbData.push({ + item: { + ...currentProject, + // prefix: project.id === projectId)} size="sm" />, + }, + items: projectItems, + }) + } + return (
{breadcrumbData.map((data, index) => ( diff --git a/apps/console-v5/src/app/components/header/header.tsx b/apps/console-v5/src/app/components/header/header.tsx index e536eee1039..1f936256cee 100644 --- a/apps/console-v5/src/app/components/header/header.tsx +++ b/apps/console-v5/src/app/components/header/header.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react' import { LogoIcon } from '@qovery/shared/ui' import { Breadcrumbs } from './breadcrumbs/breadcrumbs' import { UserMenu } from './user-menu/user-menu' @@ -21,7 +22,9 @@ export function Header() {
+ {/* Loading...
}> */} + {/* */}
diff --git a/apps/console-v5/src/routeTree.gen.ts b/apps/console-v5/src/routeTree.gen.ts index 7714ebdb6b7..1409d681791 100644 --- a/apps/console-v5/src/routeTree.gen.ts +++ b/apps/console-v5/src/routeTree.gen.ts @@ -28,7 +28,9 @@ import { Route as AuthenticatedOrganizationOrganizationIdAuditLogsRouteImport } import { Route as AuthenticatedOrganizationOrganizationIdAlertsRouteImport } from './routes/_authenticated/organization/$organizationId/alerts' import { Route as AuthenticatedOrganizationOrganizationIdClusterIdIndexRouteImport } from './routes/_authenticated/organization/$organizationId/$clusterId/index' import { Route as AuthenticatedOrganizationOrganizationIdClusterNewRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/new' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/index' import { Route as AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/$clusterId/index' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/overview' import { Route as AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/$clusterId/overview' import { Route as AuthenticatedOrganizationOrganizationIdClusterClusterIdClusterLogsRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/$clusterId/cluster-logs' import { Route as AuthenticatedOrganizationOrganizationIdClusterCreateSlugRouteRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/create/$slug/route' @@ -45,6 +47,8 @@ import { Route as AuthenticatedOrganizationOrganizationIdClusterClusterIdSetting import { Route as AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsDangerZoneRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/$clusterId/settings/danger-zone' import { Route as AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsCredentialsRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/$clusterId/settings/credentials' import { Route as AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsAdvancedSettingsRouteImport } from './routes/_authenticated/organization/$organizationId/cluster/$clusterId/settings/advanced-settings' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/index' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview' const AuthenticatedRoute = AuthenticatedRouteImport.update({ id: '/_authenticated', @@ -155,6 +159,14 @@ const AuthenticatedOrganizationOrganizationIdClusterNewRoute = path: '/cluster/new', getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, } as any) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRouteImport.update( + { + id: '/project/$projectId/', + path: '/project/$projectId/', + getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, + } as any, + ) const AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute = AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRouteImport.update( { @@ -163,6 +175,14 @@ const AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute = getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, } as any, ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRouteImport.update( + { + id: '/project/$projectId/overview', + path: '/project/$projectId/overview', + getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, + } as any, + ) const AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute = AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRouteImport.update( { @@ -303,6 +323,22 @@ const AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsAdvancedSet AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsRouteRoute, } as any, ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/', + path: '/project/$projectId/environment/$environmentId/', + getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, + } as any, + ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/overview', + path: '/project/$projectId/environment/$environmentId/overview', + getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, + } as any, + ) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -327,7 +363,9 @@ export interface FileRoutesByFullPath { '/organization/$organizationId/cluster/create/$slug': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugRouteRouteWithChildren '/organization/$organizationId/cluster/$clusterId/cluster-logs': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdClusterLogsRoute '/organization/$organizationId/cluster/$clusterId/overview': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute + '/organization/$organizationId/project/$projectId/overview': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute '/organization/$organizationId/cluster/$clusterId': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute + '/organization/$organizationId/project/$projectId': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute '/organization/$organizationId/cluster/$clusterId/settings/advanced-settings': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsAdvancedSettingsRoute '/organization/$organizationId/cluster/$clusterId/settings/credentials': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsCredentialsRoute '/organization/$organizationId/cluster/$clusterId/settings/danger-zone': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsDangerZoneRoute @@ -340,6 +378,8 @@ export interface FileRoutesByFullPath { '/organization/$organizationId/cluster/create/$slug/resources': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugResourcesRoute '/organization/$organizationId/cluster/$clusterId/settings/': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsIndexRoute '/organization/$organizationId/cluster/create/$slug/': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugIndexRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/overview': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -360,7 +400,9 @@ export interface FileRoutesByTo { '/organization/$organizationId/$clusterId': typeof AuthenticatedOrganizationOrganizationIdClusterIdIndexRoute '/organization/$organizationId/cluster/$clusterId/cluster-logs': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdClusterLogsRoute '/organization/$organizationId/cluster/$clusterId/overview': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute + '/organization/$organizationId/project/$projectId/overview': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute '/organization/$organizationId/cluster/$clusterId': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute + '/organization/$organizationId/project/$projectId': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute '/organization/$organizationId/cluster/$clusterId/settings/advanced-settings': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsAdvancedSettingsRoute '/organization/$organizationId/cluster/$clusterId/settings/credentials': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsCredentialsRoute '/organization/$organizationId/cluster/$clusterId/settings/danger-zone': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsDangerZoneRoute @@ -373,6 +415,8 @@ export interface FileRoutesByTo { '/organization/$organizationId/cluster/create/$slug/resources': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugResourcesRoute '/organization/$organizationId/cluster/$clusterId/settings': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsIndexRoute '/organization/$organizationId/cluster/create/$slug': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugIndexRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/overview': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -399,7 +443,9 @@ export interface FileRoutesById { '/_authenticated/organization/$organizationId/cluster/create/$slug': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugRouteRouteWithChildren '/_authenticated/organization/$organizationId/cluster/$clusterId/cluster-logs': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdClusterLogsRoute '/_authenticated/organization/$organizationId/cluster/$clusterId/overview': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute + '/_authenticated/organization/$organizationId/project/$projectId/overview': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute '/_authenticated/organization/$organizationId/cluster/$clusterId/': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute + '/_authenticated/organization/$organizationId/project/$projectId/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/advanced-settings': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsAdvancedSettingsRoute '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/credentials': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsCredentialsRoute '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/danger-zone': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsDangerZoneRoute @@ -412,6 +458,8 @@ export interface FileRoutesById { '/_authenticated/organization/$organizationId/cluster/create/$slug/resources': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugResourcesRoute '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/': typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsIndexRoute '/_authenticated/organization/$organizationId/cluster/create/$slug/': typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugIndexRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -438,7 +486,9 @@ export interface FileRouteTypes { | '/organization/$organizationId/cluster/create/$slug' | '/organization/$organizationId/cluster/$clusterId/cluster-logs' | '/organization/$organizationId/cluster/$clusterId/overview' + | '/organization/$organizationId/project/$projectId/overview' | '/organization/$organizationId/cluster/$clusterId' + | '/organization/$organizationId/project/$projectId' | '/organization/$organizationId/cluster/$clusterId/settings/advanced-settings' | '/organization/$organizationId/cluster/$clusterId/settings/credentials' | '/organization/$organizationId/cluster/$clusterId/settings/danger-zone' @@ -451,6 +501,8 @@ export interface FileRouteTypes { | '/organization/$organizationId/cluster/create/$slug/resources' | '/organization/$organizationId/cluster/$clusterId/settings/' | '/organization/$organizationId/cluster/create/$slug/' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/overview' + | '/organization/$organizationId/project/$projectId/environment/$environmentId' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -471,7 +523,9 @@ export interface FileRouteTypes { | '/organization/$organizationId/$clusterId' | '/organization/$organizationId/cluster/$clusterId/cluster-logs' | '/organization/$organizationId/cluster/$clusterId/overview' + | '/organization/$organizationId/project/$projectId/overview' | '/organization/$organizationId/cluster/$clusterId' + | '/organization/$organizationId/project/$projectId' | '/organization/$organizationId/cluster/$clusterId/settings/advanced-settings' | '/organization/$organizationId/cluster/$clusterId/settings/credentials' | '/organization/$organizationId/cluster/$clusterId/settings/danger-zone' @@ -484,6 +538,8 @@ export interface FileRouteTypes { | '/organization/$organizationId/cluster/create/$slug/resources' | '/organization/$organizationId/cluster/$clusterId/settings' | '/organization/$organizationId/cluster/create/$slug' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/overview' + | '/organization/$organizationId/project/$projectId/environment/$environmentId' id: | '__root__' | '/' @@ -509,7 +565,9 @@ export interface FileRouteTypes { | '/_authenticated/organization/$organizationId/cluster/create/$slug' | '/_authenticated/organization/$organizationId/cluster/$clusterId/cluster-logs' | '/_authenticated/organization/$organizationId/cluster/$clusterId/overview' + | '/_authenticated/organization/$organizationId/project/$projectId/overview' | '/_authenticated/organization/$organizationId/cluster/$clusterId/' + | '/_authenticated/organization/$organizationId/project/$projectId/' | '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/advanced-settings' | '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/credentials' | '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/danger-zone' @@ -522,6 +580,8 @@ export interface FileRouteTypes { | '/_authenticated/organization/$organizationId/cluster/create/$slug/resources' | '/_authenticated/organization/$organizationId/cluster/$clusterId/settings/' | '/_authenticated/organization/$organizationId/cluster/create/$slug/' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -666,6 +726,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdClusterNewRouteImport parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute } + '/_authenticated/organization/$organizationId/project/$projectId/': { + id: '/_authenticated/organization/$organizationId/project/$projectId/' + path: '/project/$projectId' + fullPath: '/organization/$organizationId/project/$projectId' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute + } '/_authenticated/organization/$organizationId/cluster/$clusterId/': { id: '/_authenticated/organization/$organizationId/cluster/$clusterId/' path: '/cluster/$clusterId' @@ -673,6 +740,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRouteImport parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute } + '/_authenticated/organization/$organizationId/project/$projectId/overview': { + id: '/_authenticated/organization/$organizationId/project/$projectId/overview' + path: '/project/$projectId/overview' + fullPath: '/organization/$organizationId/project/$projectId/overview' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute + } '/_authenticated/organization/$organizationId/cluster/$clusterId/overview': { id: '/_authenticated/organization/$organizationId/cluster/$clusterId/overview' path: '/cluster/$clusterId/overview' @@ -785,6 +859,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsAdvancedSettingsRouteImport parentRoute: typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdSettingsRouteRoute } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/' + path: '/project/$projectId/environment/$environmentId' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute + } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview' + path: '/project/$projectId/environment/$environmentId/overview' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/overview' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute + } } } @@ -861,7 +949,11 @@ interface AuthenticatedOrganizationOrganizationIdRouteRouteChildren { AuthenticatedOrganizationOrganizationIdClusterCreateSlugRouteRoute: typeof AuthenticatedOrganizationOrganizationIdClusterCreateSlugRouteRouteWithChildren AuthenticatedOrganizationOrganizationIdClusterClusterIdClusterLogsRoute: typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdClusterLogsRoute AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute: typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute: typeof AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute } const AuthenticatedOrganizationOrganizationIdRouteRouteChildren: AuthenticatedOrganizationOrganizationIdRouteRouteChildren = @@ -890,8 +982,16 @@ const AuthenticatedOrganizationOrganizationIdRouteRouteChildren: AuthenticatedOr AuthenticatedOrganizationOrganizationIdClusterClusterIdClusterLogsRoute, AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute: AuthenticatedOrganizationOrganizationIdClusterClusterIdOverviewRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdOverviewRoute, AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute: AuthenticatedOrganizationOrganizationIdClusterClusterIdIndexRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdIndexRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdOverviewRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdIndexRoute, } const AuthenticatedOrganizationOrganizationIdRouteRouteWithChildren = diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/index.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/index.tsx new file mode 100644 index 00000000000..fe5c800f582 --- /dev/null +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/index.tsx @@ -0,0 +1,19 @@ +import { Navigate, createFileRoute, useParams } from '@tanstack/react-router' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/' +)({ + component: RouteComponent, +}) + +function RouteComponent() { + const { organizationId, projectId, environmentId } = useParams({ strict: false }) + + return ( + + ) +} diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview.tsx new file mode 100644 index 00000000000..8f21d6b85cc --- /dev/null +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview.tsx @@ -0,0 +1,31 @@ +import { createFileRoute, useParams } from '@tanstack/react-router' +import { Suspense } from 'react' +import { useEnvironment } from '@qovery/domains/environments/feature' +import { LoaderSpinner } from '@qovery/shared/ui' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview' +)({ + component: RouteComponent, +}) + +function EnvironmentOverview() { + const { environmentId } = useParams({ strict: false }) + const { data: environment } = useEnvironment({ environmentId, suspense: true }) + + return
Environment: {environment?.name}
+} + +function RouteComponent() { + return ( + + +
+ } + > + + + ) +} diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/index.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/index.tsx new file mode 100644 index 00000000000..120ef4af74e --- /dev/null +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/index.tsx @@ -0,0 +1,13 @@ +import { Navigate, createFileRoute, useParams } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated/organization/$organizationId/project/$projectId/')({ + component: RouteComponent, +}) + +function RouteComponent() { + const { organizationId, projectId } = useParams({ strict: false }) + + return ( + + ) +} diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx new file mode 100644 index 00000000000..854d6cbd02f --- /dev/null +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx @@ -0,0 +1,211 @@ +import { Link, createFileRoute, useParams } from '@tanstack/react-router' +import { type Environment, type EnvironmentModeEnum } from 'qovery-typescript-axios' +import { Suspense, useMemo } from 'react' +import { match } from 'ts-pattern' +import { ClusterAvatar } from '@qovery/domains/clusters/feature' +import { useClusters } from '@qovery/domains/clusters/feature' +import { CreateCloneEnvironmentModal, useEnvironments } from '@qovery/domains/environments/feature' +import { useProject } from '@qovery/domains/projects/feature' +import { useServices } from '@qovery/domains/services/feature' +import { Button, EnvType, Heading, Icon, LoaderSpinner, Section, TablePrimitives, useModal } from '@qovery/shared/ui' +import { pluralize } from '@qovery/shared/util-js' + +const { Table } = TablePrimitives + +export const Route = createFileRoute('/_authenticated/organization/$organizationId/project/$projectId/overview')({ + component: RouteComponent, +}) + +function EnvRow({ environment }: { environment: Environment }) { + const { organizationId, projectId } = useParams({ strict: false }) + const { data: clusters } = useClusters({ organizationId, suspense: true }) + const { data: services } = useServices({ environmentId: environment.id, suspense: true }) + + return ( + + +
+ + {environment.name} + + + {services?.length} {pluralize(services?.length ?? 0, 'service')} + +
+
+ + +
+ cluster.id === environment.cluster_id)} size="sm" /> + {clusters?.find((cluster) => cluster.id === environment.cluster_id)?.name} +
+
+ + +
+ ) +} + +function EnvironmentSection({ + type, + items, + onCreateEnvClicked, +}: { + type: 'production' | 'staging' | 'development' | 'ephemeral' + items: Environment[] + onCreateEnvClicked: () => void +}) { + const title = match(type) + .with('production', () => 'Production') + .with('staging', () => 'Staging') + .with('development', () => 'Development') + .with('ephemeral', () => 'Ephemeral') + .exhaustive() + + return ( +
+
+ + {title} +
+ {items.length === 0 ? ( +
+ + No {title.toLowerCase()} environment created yet + +
+ ) : ( +
+ + +
+ } + > + + + + Environment + + Last operation + + + Cluster + + + Last update + + + Actions + + + + + + {items.map((environment) => ( + + ))} + + + + + )} +
+ ) +} + +function ProjectOverview() { + const { openModal, closeModal } = useModal() + const { organizationId, projectId } = useParams({ strict: false }) + const { data: project } = useProject({ organizationId, projectId, suspense: true }) + const { data: environments } = useEnvironments({ projectId, suspense: true }) + + const groupedEnvs = useMemo(() => { + return environments?.reduce((acc, env) => { + acc.set(env.mode, [...(acc.get(env.mode) || []), env]) + return acc + }, new Map()) + }, [environments]) + + const onCreateEnvClicked = () => { + openModal({ + content: ( + + ), + options: { + fakeModal: true, + }, + }) + } + + return ( +
+
+
+
+ {project?.name} + +
+
+
+
+ + + + +
+
+
+ ) +} + +function RouteComponent() { + return ( + + + + } + > + + + ) +} diff --git a/apps/console-v5/src/routes/_authenticated/organization/route.tsx b/apps/console-v5/src/routes/_authenticated/organization/route.tsx index 9909b48001d..28756feb2f1 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/route.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/route.tsx @@ -1,6 +1,7 @@ import { type IconName } from '@fortawesome/fontawesome-common-types' import { Outlet, createFileRoute, useLocation, useMatches, useParams } from '@tanstack/react-router' -import { Icon, Navbar } from '@qovery/shared/ui' +import { Suspense } from 'react' +import { Icon, LoaderSpinner, Navbar } from '@qovery/shared/ui' import { queries } from '@qovery/state/util-queries' import Header from '../../../app/components/header/header' import { type FileRouteTypes } from '../../../routeTree.gen' @@ -85,6 +86,24 @@ const CLUSTER_TABS: NavigationTab[] = [ }, ] +const PROJECT_TABS: NavigationTab[] = [ + { + id: 'overview', + label: 'Overview', + iconName: 'table-layout', + routeId: '/_authenticated/organization/$organizationId/project/$projectId/overview', + }, +] + +const ENVIRONMENT_TABS: NavigationTab[] = [ + { + id: 'overview', + label: 'Overview', + iconName: 'table-layout', + routeId: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview', + }, +] + function createRoutePatternRegex(routeIdPattern: string): RegExp { const patternPath = routeIdPattern.replace('/_authenticated/organization', '/organization') return new RegExp('^' + patternPath.replace(/\$(\w+)/g, '[^/]+') + '(/.*)?$') @@ -107,6 +126,18 @@ const NAVIGATION_CONTEXTS: Array<{ tabs: NavigationTab[] paramNames: string[] }> = [ + { + type: 'environment', + routeIdPattern: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId', + tabs: ENVIRONMENT_TABS, + paramNames: ['organizationId', 'projectId', 'environmentId'], + }, + { + type: 'project', + routeIdPattern: '/_authenticated/organization/$organizationId/project/$projectId', + tabs: PROJECT_TABS, + paramNames: ['organizationId', 'projectId'], + }, { type: 'cluster', routeIdPattern: '/_authenticated/organization/$organizationId/cluster/$clusterId', @@ -259,6 +290,14 @@ function useBypassLayout(): boolean { ) } +function MainLoader() { + return ( +
+ +
+ ) +} + function OrganizationRoute() { const navigationContext = useNavigationContext() const activeTabId = useActiveTabId(navigationContext) @@ -273,17 +312,21 @@ function OrganizationRoute() {
{/* TODO: Conflicts with body main:not(.h-screen, .layout-onboarding) */}
-
- -
- - {navigationContext && } - -
- -
- -
+ }> + <> +
+ +
+ + {navigationContext && } + +
+ +
+ +
+ +
) diff --git a/libs/domains/environments/feature/src/lib/create-clone-environment-modal/create-clone-environment-modal.tsx b/libs/domains/environments/feature/src/lib/create-clone-environment-modal/create-clone-environment-modal.tsx index 31a50d0aa91..eea068143c8 100644 --- a/libs/domains/environments/feature/src/lib/create-clone-environment-modal/create-clone-environment-modal.tsx +++ b/libs/domains/environments/feature/src/lib/create-clone-environment-modal/create-clone-environment-modal.tsx @@ -1,3 +1,4 @@ +import { useNavigate } from '@tanstack/react-router' import { type CreateEnvironmentModeEnum, type Environment, @@ -6,11 +7,9 @@ import { } from 'qovery-typescript-axios' import { type FormEvent } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' -import { useNavigate } from 'react-router-dom' import { P, match } from 'ts-pattern' import { useClusters } from '@qovery/domains/clusters/feature' import { useProjects } from '@qovery/domains/projects/feature' -import { SERVICES_GENERAL_URL, SERVICES_URL } from '@qovery/shared/routes' import { ExternalLink, Icon, InputSelect, InputText, ModalCrud, useModal } from '@qovery/shared/ui' import { EnvironmentMode } from '../environment-mode/environment-mode' import { useCloneEnvironment } from '../hooks/use-clone-environment/use-clone-environment' @@ -60,7 +59,9 @@ export function CreateCloneEnvironmentModal({ }, }) - navigate(SERVICES_URL(organizationId, project_id, result.id) + SERVICES_GENERAL_URL) + navigate({ + to: `/organization/${organizationId}/project/${project_id}/environment/${result.id}/overview`, + }) } else { const result = await createEnvironment({ projectId: project_id, @@ -70,7 +71,9 @@ export function CreateCloneEnvironmentModal({ cluster: cluster, }, }) - navigate(SERVICES_URL(organizationId, project_id, result.id) + SERVICES_GENERAL_URL) + navigate({ + to: `/organization/${organizationId}/project/${project_id}/environment/${result.id}/overview`, + }) } onClose() }) diff --git a/libs/domains/environments/feature/src/lib/hooks/use-environment/use-environment.ts b/libs/domains/environments/feature/src/lib/hooks/use-environment/use-environment.ts index eaa8d5b54cb..34b91c2f523 100644 --- a/libs/domains/environments/feature/src/lib/hooks/use-environment/use-environment.ts +++ b/libs/domains/environments/feature/src/lib/hooks/use-environment/use-environment.ts @@ -3,13 +3,15 @@ import { queries } from '@qovery/state/util-queries' export interface UseEnvironmentProps { environmentId?: string + suspense?: boolean } -export function useEnvironment({ environmentId }: UseEnvironmentProps) { +export function useEnvironment({ environmentId, suspense = false }: UseEnvironmentProps) { return useQuery({ // eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion ...queries.environments.details({ environmentId: environmentId!! }), enabled: Boolean(environmentId), + suspense, }) } diff --git a/libs/domains/environments/feature/src/lib/hooks/use-environments/use-environments.ts b/libs/domains/environments/feature/src/lib/hooks/use-environments/use-environments.ts index f2edd603f1a..a830235a316 100644 --- a/libs/domains/environments/feature/src/lib/hooks/use-environments/use-environments.ts +++ b/libs/domains/environments/feature/src/lib/hooks/use-environments/use-environments.ts @@ -6,9 +6,10 @@ import { queries } from '@qovery/state/util-queries' export interface UseEnvironmentsProps { projectId: string + suspense?: boolean } -export function useEnvironments({ projectId }: UseEnvironmentsProps) { +export function useEnvironments({ projectId, suspense = false }: UseEnvironmentsProps) { const { data: environments, isLoading: isEnvironmentsLoading, @@ -21,6 +22,7 @@ export function useEnvironments({ projectId }: UseEnvironmentsProps) { }, enabled: projectId !== '', retry: 3, + suspense, }) const runningStatusResults = useQueries({ diff --git a/libs/domains/projects/feature/src/lib/hooks/use-project/use-project.ts b/libs/domains/projects/feature/src/lib/hooks/use-project/use-project.ts index 57f7b4315b7..4068bc6e006 100644 --- a/libs/domains/projects/feature/src/lib/hooks/use-project/use-project.ts +++ b/libs/domains/projects/feature/src/lib/hooks/use-project/use-project.ts @@ -4,12 +4,14 @@ import { queries } from '@qovery/state/util-queries' export interface UseProjectProps { organizationId: string projectId: string + suspense?: boolean } -export function useProject({ organizationId, projectId }: UseProjectProps) { +export function useProject({ organizationId, projectId, suspense = false }: UseProjectProps) { return useQuery({ ...queries.projects.list({ organizationId }), select: (data) => data?.find((project) => project.id === projectId), + suspense, }) } diff --git a/libs/domains/projects/feature/src/lib/project-list/project-list.tsx b/libs/domains/projects/feature/src/lib/project-list/project-list.tsx index 40fe5267724..84150b74047 100644 --- a/libs/domains/projects/feature/src/lib/project-list/project-list.tsx +++ b/libs/domains/projects/feature/src/lib/project-list/project-list.tsx @@ -1,4 +1,4 @@ -import { useParams } from '@tanstack/react-router' +import { Link, useParams } from '@tanstack/react-router' import clsx from 'clsx' import { Button, EmptyState, Heading, Icon, Section, useModal } from '@qovery/shared/ui' import { twMerge } from '@qovery/shared/util-js' @@ -10,7 +10,6 @@ export function ProjectList() { const { organizationId = '' }: { organizationId: string } = useParams({ strict: false }) const { data: projects = [] } = useProjects({ organizationId, suspense: true }) const { openModal, closeModal } = useModal() - const createProjectModal = () => { openModal({ content: , @@ -48,8 +47,10 @@ export function ProjectList() { )} > {projects?.map((project) => ( - + ))} )} diff --git a/libs/domains/services/feature/src/lib/hooks/use-services/use-services.ts b/libs/domains/services/feature/src/lib/hooks/use-services/use-services.ts index 9295277de16..e62969706a9 100644 --- a/libs/domains/services/feature/src/lib/hooks/use-services/use-services.ts +++ b/libs/domains/services/feature/src/lib/hooks/use-services/use-services.ts @@ -6,9 +6,10 @@ import { queries } from '@qovery/state/util-queries' export interface UseServicesProps { environmentId?: string + suspense?: boolean } -export function useServices({ environmentId }: UseServicesProps) { +export function useServices({ environmentId, suspense = false }: UseServicesProps) { const { data: services, isLoading: isServicesLoading } = useQuery({ ...queries.services.list(environmentId!), select(services) { @@ -16,6 +17,7 @@ export function useServices({ environmentId }: UseServicesProps) { return services }, enabled: Boolean(environmentId), + suspense, }) const runningStatusResults = useQueries({ diff --git a/libs/shared/ui/src/index.ts b/libs/shared/ui/src/index.ts index 081ddbd1ad6..3d66f9bfdd6 100644 --- a/libs/shared/ui/src/index.ts +++ b/libs/shared/ui/src/index.ts @@ -116,6 +116,7 @@ export * from './lib/components/multiple-selector/multiple-selector' export * from './lib/components/logo/logo' export * from './lib/components/logo-branded/logo-branded' export * from './lib/components/sidebar/sidebar' +export * from './lib/components/env-type/env-type' export * from './lib/utils/toast' export * from './lib/utils/toast-error' export * from './lib/utils/ansi' diff --git a/libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx b/libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx new file mode 100644 index 00000000000..e2eb73bb417 --- /dev/null +++ b/libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta } from '@storybook/react-webpack5' +import { EnvType } from './env-type' + +const Story: Meta = { + component: EnvType, + title: 'EnvType', + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} +export default Story + +export const Primary = { + render: () => ( +
+
+
+ + Production +
+
+ + Staging +
+
+ + Development +
+
+ + Ephemeral +
+
+
+
+ + Production +
+
+ + Staging +
+
+ + Development +
+
+ + Ephemeral +
+
+
+ ), +} diff --git a/libs/shared/ui/src/lib/components/env-type/env-type.tsx b/libs/shared/ui/src/lib/components/env-type/env-type.tsx new file mode 100644 index 00000000000..0a5d17db39c --- /dev/null +++ b/libs/shared/ui/src/lib/components/env-type/env-type.tsx @@ -0,0 +1,43 @@ +import { type VariantProps, cva } from 'class-variance-authority' +import { type ComponentPropsWithoutRef } from 'react' +import { match } from 'ts-pattern' +import { twMerge } from '@qovery/shared/util-js' + +const _envTypeVariants = cva( + ['inline-flex', 'items-center', 'justify-center', 'border', 'border-neutral', 'font-semibold'], + { + variants: { + type: { + production: ['border-negative-strong', 'bg-surface-negative-subtle', 'text-negative'], + ephemeral: ['border-accent1-strong', 'bg-surface-accent1-component', 'text-accent1'], + staging: ['border-neutral-strong', 'bg-surface-neutral-component', 'text-neutral'], + development: ['border-neutral-component', 'bg-surface-neutral-subtle', 'text-neutral-subtle'], + }, + size: { + sm: ['text-2xs', 'h-4', 'w-4', 'rounded-[3px]'], + lg: ['text-xs', 'h-6', 'w-6', 'rounded-[4px]'], + }, + }, + defaultVariants: { + type: 'production', + size: 'sm', + }, + } +) + +export interface EnvTypeProps extends VariantProps, ComponentPropsWithoutRef<'div'> { + type: 'production' | 'staging' | 'development' | 'ephemeral' +} + +export const EnvType = ({ type, size, className }: EnvTypeProps) => { + return ( +
+ {match(type) + .with('production', () => 'P') + .with('staging', () => 'S') + .with('development', () => 'D') + .with('ephemeral', () => 'E') + .exhaustive()} +
+ ) +} diff --git a/libs/shared/ui/src/lib/components/table-primitives/table-primitives.tsx b/libs/shared/ui/src/lib/components/table-primitives/table-primitives.tsx index de21b85204f..99161d71169 100644 --- a/libs/shared/ui/src/lib/components/table-primitives/table-primitives.tsx +++ b/libs/shared/ui/src/lib/components/table-primitives/table-primitives.tsx @@ -12,7 +12,7 @@ const TableRoot = forwardRef, TableRootProps>(function Table return ( {children} @@ -86,7 +86,7 @@ const TableColumnHeaderCell = forwardRef, TableColumnHeaderCell ref ) { return ( - ) From 972cf4d443cf990689538b80baf93e4b39430d87 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Mon, 19 Jan 2026 14:50:49 +0100 Subject: [PATCH 2/8] impr: remove duplicated component --- .../project/$projectId/overview.tsx | 26 ++++----- .../lib/environment-mode/environment-mode.tsx | 2 +- libs/shared/ui/src/index.ts | 1 - .../components/env-type/env-type.stories.tsx | 58 ------------------- .../src/lib/components/env-type/env-type.tsx | 43 -------------- 5 files changed, 14 insertions(+), 116 deletions(-) delete mode 100644 libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx delete mode 100644 libs/shared/ui/src/lib/components/env-type/env-type.tsx diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx index 854d6cbd02f..da88d3373bf 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx @@ -4,10 +4,10 @@ import { Suspense, useMemo } from 'react' import { match } from 'ts-pattern' import { ClusterAvatar } from '@qovery/domains/clusters/feature' import { useClusters } from '@qovery/domains/clusters/feature' -import { CreateCloneEnvironmentModal, useEnvironments } from '@qovery/domains/environments/feature' +import { CreateCloneEnvironmentModal, EnvironmentMode, useEnvironments } from '@qovery/domains/environments/feature' import { useProject } from '@qovery/domains/projects/feature' import { useServices } from '@qovery/domains/services/feature' -import { Button, EnvType, Heading, Icon, LoaderSpinner, Section, TablePrimitives, useModal } from '@qovery/shared/ui' +import { Button, Heading, Icon, LoaderSpinner, Section, TablePrimitives, useModal } from '@qovery/shared/ui' import { pluralize } from '@qovery/shared/util-js' const { Table } = TablePrimitives @@ -55,26 +55,26 @@ function EnvironmentSection({ items, onCreateEnvClicked, }: { - type: 'production' | 'staging' | 'development' | 'ephemeral' + type: EnvironmentModeEnum items: Environment[] onCreateEnvClicked: () => void }) { const title = match(type) - .with('production', () => 'Production') - .with('staging', () => 'Staging') - .with('development', () => 'Development') - .with('ephemeral', () => 'Ephemeral') + .with('PRODUCTION', () => 'Production') + .with('STAGING', () => 'Staging') + .with('DEVELOPMENT', () => 'Development') + .with('PREVIEW', () => 'Ephemeral') .exhaustive() return (
- + {title}
{items.length === 0 ? (
- + No {title.toLowerCase()} environment created yet
diff --git a/libs/domains/environments/feature/src/lib/environment-mode/environment-mode.tsx b/libs/domains/environments/feature/src/lib/environment-mode/environment-mode.tsx index 20575f5b13f..979bf0aa3af 100644 --- a/libs/domains/environments/feature/src/lib/environment-mode/environment-mode.tsx +++ b/libs/domains/environments/feature/src/lib/environment-mode/environment-mode.tsx @@ -57,7 +57,7 @@ export const EnvironmentMode = forwardRef, EnvironmentM className={twMerge(className, environmentModeVariants({ variant }))} {...props} > - {variant === 'full' ? 'Preview' : 'V'} + {variant === 'full' ? 'Ephemeral' : 'E'} )) .with('STAGING', () => ( diff --git a/libs/shared/ui/src/index.ts b/libs/shared/ui/src/index.ts index 3d66f9bfdd6..081ddbd1ad6 100644 --- a/libs/shared/ui/src/index.ts +++ b/libs/shared/ui/src/index.ts @@ -116,7 +116,6 @@ export * from './lib/components/multiple-selector/multiple-selector' export * from './lib/components/logo/logo' export * from './lib/components/logo-branded/logo-branded' export * from './lib/components/sidebar/sidebar' -export * from './lib/components/env-type/env-type' export * from './lib/utils/toast' export * from './lib/utils/toast-error' export * from './lib/utils/ansi' diff --git a/libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx b/libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx deleted file mode 100644 index e2eb73bb417..00000000000 --- a/libs/shared/ui/src/lib/components/env-type/env-type.stories.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type { Meta } from '@storybook/react-webpack5' -import { EnvType } from './env-type' - -const Story: Meta = { - component: EnvType, - title: 'EnvType', - decorators: [ - (Story) => ( -
- -
- ), - ], -} -export default Story - -export const Primary = { - render: () => ( -
-
-
- - Production -
-
- - Staging -
-
- - Development -
-
- - Ephemeral -
-
-
-
- - Production -
-
- - Staging -
-
- - Development -
-
- - Ephemeral -
-
-
- ), -} diff --git a/libs/shared/ui/src/lib/components/env-type/env-type.tsx b/libs/shared/ui/src/lib/components/env-type/env-type.tsx deleted file mode 100644 index 0a5d17db39c..00000000000 --- a/libs/shared/ui/src/lib/components/env-type/env-type.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { type VariantProps, cva } from 'class-variance-authority' -import { type ComponentPropsWithoutRef } from 'react' -import { match } from 'ts-pattern' -import { twMerge } from '@qovery/shared/util-js' - -const _envTypeVariants = cva( - ['inline-flex', 'items-center', 'justify-center', 'border', 'border-neutral', 'font-semibold'], - { - variants: { - type: { - production: ['border-negative-strong', 'bg-surface-negative-subtle', 'text-negative'], - ephemeral: ['border-accent1-strong', 'bg-surface-accent1-component', 'text-accent1'], - staging: ['border-neutral-strong', 'bg-surface-neutral-component', 'text-neutral'], - development: ['border-neutral-component', 'bg-surface-neutral-subtle', 'text-neutral-subtle'], - }, - size: { - sm: ['text-2xs', 'h-4', 'w-4', 'rounded-[3px]'], - lg: ['text-xs', 'h-6', 'w-6', 'rounded-[4px]'], - }, - }, - defaultVariants: { - type: 'production', - size: 'sm', - }, - } -) - -export interface EnvTypeProps extends VariantProps, ComponentPropsWithoutRef<'div'> { - type: 'production' | 'staging' | 'development' | 'ephemeral' -} - -export const EnvType = ({ type, size, className }: EnvTypeProps) => { - return ( -
- {match(type) - .with('production', () => 'P') - .with('staging', () => 'S') - .with('development', () => 'D') - .with('ephemeral', () => 'E') - .exhaustive()} -
- ) -} From ab8b3a26b510162a4595bee624cc79aa1ce825d4 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Mon, 19 Jan 2026 16:32:26 +0100 Subject: [PATCH 3/8] impr: UI tweaks for 'Dismiss' modal --- .../ui/src/lib/components/modal-alert/modal-alert.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/shared/ui/src/lib/components/modal-alert/modal-alert.tsx b/libs/shared/ui/src/lib/components/modal-alert/modal-alert.tsx index c1550878b65..a1720ba60c7 100644 --- a/libs/shared/ui/src/lib/components/modal-alert/modal-alert.tsx +++ b/libs/shared/ui/src/lib/components/modal-alert/modal-alert.tsx @@ -16,11 +16,11 @@ export function ModalAlert(props: ModalAlertProps) {
-

Discard changes?

-

Are you sure you want to discard your changes?

+

Discard changes?

+

Are you sure you want to discard your changes?

- + + {timeAgo(new Date(environment.updated_at ?? Date.now()))} ago + ) @@ -82,7 +85,7 @@ function EnvironmentSection({
) : ( -
+
From 05f7e8db5d2db77adfefbf6a1a5e7e91ac210df8 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Wed, 21 Jan 2026 18:45:36 +0100 Subject: [PATCH 5/8] feat: implement new environment overview API endpoint --- .../project/$projectId/overview.tsx | 140 ++++++++++-------- .../organization/$organizationId/route.tsx | 36 ++++- .../src/lib/cluster-avatar/cluster-avatar.tsx | 4 +- .../src/lib/domains-projects-data-access.ts | 8 + libs/domains/projects/feature/src/index.ts | 1 + .../use-environments-overview.ts | 18 +++ package.json | 2 +- yarn.lock | 10 +- 8 files changed, 148 insertions(+), 71 deletions(-) create mode 100644 libs/domains/projects/feature/src/lib/hooks/use-environments-overview/use-environments-overview.ts diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx index 181ab74bc66..a68cbc1c42f 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx @@ -1,15 +1,24 @@ import { Link, createFileRoute, useParams } from '@tanstack/react-router' -import { type Environment, type EnvironmentModeEnum } from 'qovery-typescript-axios' +import { type EnvironmentModeEnum, type EnvironmentOverviewResponse } from 'qovery-typescript-axios' import { Suspense, useMemo } from 'react' import { match } from 'ts-pattern' import { ClusterAvatar } from '@qovery/domains/clusters/feature' -import { useClusters } from '@qovery/domains/clusters/feature' import { CreateCloneEnvironmentModal, EnvironmentMode, useEnvironments } from '@qovery/domains/environments/feature' -import { useProject } from '@qovery/domains/projects/feature' -import { useServices } from '@qovery/domains/services/feature' -import { Button, Heading, Icon, LoaderSpinner, Section, TablePrimitives, useModal } from '@qovery/shared/ui' +import { useEnvironmentsOverview, useProject } from '@qovery/domains/projects/feature' +import { + Button, + Heading, + Icon, + LoaderSpinner, + Section, + StatusChip, + TablePrimitives, + Tooltip, + Truncate, + useModal, +} from '@qovery/shared/ui' import { timeAgo } from '@qovery/shared/util-dates' -import { pluralize } from '@qovery/shared/util-js' +import { pluralize, twMerge } from '@qovery/shared/util-js' const { Table } = TablePrimitives @@ -17,38 +26,57 @@ export const Route = createFileRoute('/_authenticated/organization/$organization component: RouteComponent, }) -function EnvRow({ environment }: { environment: Environment }) { +const gridLayoutClassName = 'grid grid-cols-[3fr_2fr_2fr_180px_100px]' + +function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) { const { organizationId, projectId } = useParams({ strict: false }) - const { data: clusters } = useClusters({ organizationId, suspense: true }) - const { data: services } = useServices({ environmentId: environment.id, suspense: true }) + const { data: environments = [] } = useEnvironments({ projectId, suspense: true }) + const environment = environments.find((env) => env.id === overview.id) + const runningStatus = environment?.runningStatus return ( - + -
+
- {environment.name} + - - {services?.length} {pluralize(services?.length ?? 0, 'service')} - +
+ + {overview.service_count} {pluralize(overview.service_count, 'service')} + + {runningStatus && ( + + + + )} +
- -
- cluster.id === environment.cluster_id)} size="sm" /> - {clusters?.find((cluster) => cluster.id === environment.cluster_id)?.name} +
+ {timeAgo(new Date(overview.deployment_status?.last_deployment_date ?? Date.now()))} ago +
- {timeAgo(new Date(environment.updated_at ?? Date.now()))} ago + {overview.cluster && ( +
+ + {overview.cluster?.name} +
+ )} +
+ +
{timeAgo(new Date(overview.updated_at ?? Date.now()))} ago
+
+ +
- ) } @@ -59,7 +87,7 @@ function EnvironmentSection({ onCreateEnvClicked, }: { type: EnvironmentModeEnum - items: Environment[] + items: EnvironmentOverviewResponse[] onCreateEnvClicked: () => void }) { const title = match(type) @@ -86,39 +114,33 @@ function EnvironmentSection({
) : (
- - -
- } - > - - - - Environment - - Last operation - - - Cluster - - - Last update - - - Actions - - - + + + + + Environment + + + Last operation + + + Cluster + + + Last update + + + Actions + + + - - {items.map((environment) => ( - - ))} - - - + + {items.map((environmentOverview) => ( + + ))} + +
)}
@@ -129,14 +151,14 @@ function ProjectOverview() { const { openModal, closeModal } = useModal() const { organizationId, projectId } = useParams({ strict: false }) const { data: project } = useProject({ organizationId, projectId, suspense: true }) - const { data: environments } = useEnvironments({ projectId, suspense: true }) + const { data: environmentsOverview } = useEnvironmentsOverview({ projectId, suspense: true }) const groupedEnvs = useMemo(() => { - return environments?.reduce((acc, env) => { + return environmentsOverview?.reduce((acc, env) => { acc.set(env.mode, [...(acc.get(env.mode) || []), env]) return acc - }, new Map()) - }, [environments]) + }, new Map()) + }, [environmentsOverview]) const onCreateEnvClicked = () => { openModal({ diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/route.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/route.tsx index f19e951f9f9..638ed9fb947 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/route.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/route.tsx @@ -1,6 +1,9 @@ -import { Outlet, createFileRoute } from '@tanstack/react-router' +import { Outlet, createFileRoute, useParams } from '@tanstack/react-router' import { Suspense } from 'react' +import { memo } from 'react' +import { useClusters } from '@qovery/domains/clusters/feature' import { LoaderSpinner } from '@qovery/shared/ui' +import { StatusWebSocketListener } from '@qovery/shared/util-web-sockets' import { queries } from '@qovery/state/util-queries' export const Route = createFileRoute('/_authenticated/organization/$organizationId')({ @@ -28,10 +31,35 @@ const Loader = () => { ) } +const StatusWebSocketListenerMemo = memo(StatusWebSocketListener) + function RouteComponent() { + const { organizationId = '', projectId = '', environmentId = '', versionId = '' } = useParams({ strict: false }) + const { data: clusters } = useClusters({ organizationId }) + return ( - }> - - + <> + }> + + + + {/** + * Here we are limited by the websocket API which requires a clusterId + * We need to instantiate one hook per clusterId to get the complete environment statuses of the page + */ + clusters?.map( + ({ id }) => + organizationId && ( + + ) + )} + ) } diff --git a/libs/domains/clusters/feature/src/lib/cluster-avatar/cluster-avatar.tsx b/libs/domains/clusters/feature/src/lib/cluster-avatar/cluster-avatar.tsx index d3d9e87ebf0..6d6edc26c26 100644 --- a/libs/domains/clusters/feature/src/lib/cluster-avatar/cluster-avatar.tsx +++ b/libs/domains/clusters/feature/src/lib/cluster-avatar/cluster-avatar.tsx @@ -1,11 +1,11 @@ -import { type CloudProviderEnum, type Cluster } from 'qovery-typescript-axios' +import { type CloudProviderEnum, type Cluster, type ClusterOverviewResponse } from 'qovery-typescript-axios' import { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react' import { match } from 'ts-pattern' import { Avatar, Icon } from '@qovery/shared/ui' export interface ClusterAvatarProps extends Omit, 'fallback'> { cloudProvider?: CloudProviderEnum - cluster?: Cluster + cluster?: Cluster | ClusterOverviewResponse } export const ClusterAvatar = forwardRef, ClusterAvatarProps>(function ClusterAvatar( diff --git a/libs/domains/projects/data-access/src/lib/domains-projects-data-access.ts b/libs/domains/projects/data-access/src/lib/domains-projects-data-access.ts index f356b03b533..d5bf7177019 100644 --- a/libs/domains/projects/data-access/src/lib/domains-projects-data-access.ts +++ b/libs/domains/projects/data-access/src/lib/domains-projects-data-access.ts @@ -1,5 +1,6 @@ import { createQueryKeys } from '@lukemorales/query-key-factory' import { + EnvironmentsApi, ProjectDeploymentRuleApi, type ProjectDeploymentRuleRequest, type ProjectDeploymentRulesPriorityOrderRequest, @@ -11,6 +12,7 @@ import { const projectsApi = new ProjectsApi() const projectMainCalls = new ProjectMainCallsApi() const deploymentRulesApi = new ProjectDeploymentRuleApi() +const environmentsApi = new EnvironmentsApi() export const projects = createQueryKeys('projects', { list: ({ organizationId }: { organizationId: string }) => ({ @@ -19,6 +21,12 @@ export const projects = createQueryKeys('projects', { return (await projectsApi.listProject(organizationId)).data.results }, }), + environmentsOverview: ({ projectId }: { projectId: string }) => ({ + queryKey: [projectId, 'environments-overview'], + async queryFn() { + return (await environmentsApi.getProjectEnvironmentsOverview(projectId)).data.results + }, + }), listDeploymentRules: ({ projectId }: { projectId: string }) => ({ queryKey: [projectId], async queryFn() { diff --git a/libs/domains/projects/feature/src/index.ts b/libs/domains/projects/feature/src/index.ts index 1e512ee3b14..c01f5f7c528 100644 --- a/libs/domains/projects/feature/src/index.ts +++ b/libs/domains/projects/feature/src/index.ts @@ -12,3 +12,4 @@ export * from './lib/hooks/use-create-deployment-rule/use-create-deployment-rule export * from './lib/hooks/use-edit-deployment-rule/use-edit-deployment-rule' export * from './lib/hooks/use-delete-deployment-rule/use-delete-deployment-rule' export * from './lib/hooks/use-edit-deployment-rules-priority-order/use-edit-deployment-rules-priority-order' +export * from './lib/hooks/use-environments-overview/use-environments-overview' diff --git a/libs/domains/projects/feature/src/lib/hooks/use-environments-overview/use-environments-overview.ts b/libs/domains/projects/feature/src/lib/hooks/use-environments-overview/use-environments-overview.ts new file mode 100644 index 00000000000..e923feb0fb4 --- /dev/null +++ b/libs/domains/projects/feature/src/lib/hooks/use-environments-overview/use-environments-overview.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query' +import { queries } from '@qovery/state/util-queries' + +interface UseEnvironmentsOverviewProps { + projectId: string + enabled?: boolean + suspense?: boolean +} + +export function useEnvironmentsOverview({ projectId, enabled, suspense = false }: UseEnvironmentsOverviewProps) { + return useQuery({ + ...queries.projects.environmentsOverview({ projectId }), + enabled, + suspense, + }) +} + +export default useEnvironmentsOverview diff --git a/package.json b/package.json index e850bf079aa..405fc561168 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "mermaid": "^11.6.0", "monaco-editor": "0.53.0", "posthog-js": "^1.260.1", - "qovery-typescript-axios": "^1.1.804", + "qovery-typescript-axios": "^1.1.811", "react": "18.3.1", "react-country-flag": "^3.0.2", "react-datepicker": "^4.12.0", diff --git a/yarn.lock b/yarn.lock index d9995031405..f90059155fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6214,7 +6214,7 @@ __metadata: prettier: ^3.2.5 prettier-plugin-tailwindcss: ^0.5.14 pretty-quick: ^4.0.0 - qovery-typescript-axios: ^1.1.804 + qovery-typescript-axios: ^1.1.811 qovery-ws-typescript-axios: ^0.1.420 react: 18.3.1 react-country-flag: ^3.0.2 @@ -25894,12 +25894,12 @@ __metadata: languageName: node linkType: hard -"qovery-typescript-axios@npm:^1.1.804": - version: 1.1.804 - resolution: "qovery-typescript-axios@npm:1.1.804" +"qovery-typescript-axios@npm:^1.1.811": + version: 1.1.811 + resolution: "qovery-typescript-axios@npm:1.1.811" dependencies: axios: 1.12.2 - checksum: 0f978e5b4cebe257b7934cb97cdbc09671d716f7da8e5842cd37b1548489d9bbc769ac143ead236f144b1e05eb893fefa35b8c6f02962d971f8c5259fc9329e1 + checksum: 4bce8b26cccbdeea0b6782a0e5d98dc0734704ca114532aa1a329f39c8a4d1c91e95a30ad2e18498c6be6927c9f8921a2d233359c68805506432bf102576a2c0 languageName: node linkType: hard From 429720167d213081233d143948b1666f22821186 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Thu, 22 Jan 2026 13:15:39 +0100 Subject: [PATCH 6/8] impr: add last operation to environment table --- .../$organizationId/project/$projectId/overview.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx index a68cbc1c42f..69c6ad3697c 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx @@ -18,7 +18,7 @@ import { useModal, } from '@qovery/shared/ui' import { timeAgo } from '@qovery/shared/util-dates' -import { pluralize, twMerge } from '@qovery/shared/util-js' +import { pluralize, twMerge, upperCaseFirstLetter } from '@qovery/shared/util-js' const { Table } = TablePrimitives @@ -59,7 +59,14 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
- {timeAgo(new Date(overview.deployment_status?.last_deployment_date ?? Date.now()))} ago +
+ + {upperCaseFirstLetter(overview.deployment_status?.last_deployment_state.replace('_', ' '))} + + + {timeAgo(new Date(overview.deployment_status?.last_deployment_date ?? Date.now()))} ago + +
From e726fec1d1e9409801a2c984feed7c66a31603db Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Thu, 22 Jan 2026 15:03:03 +0100 Subject: [PATCH 7/8] impr(new-nav): add actions to table --- .../project/$projectId/overview.tsx | 31 ++++++++++++++++--- .../environment-action-toolbar.tsx | 11 +++---- .../use-cancel-deployment-environment.ts | 4 +-- .../use-delete-environment.ts | 4 +-- .../use-deploy-environment.ts | 4 +-- .../use-stop-environment.ts | 4 +-- .../use-uninstall-environment.ts | 4 +-- 7 files changed, 41 insertions(+), 21 deletions(-) diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx index 69c6ad3697c..c47f76f4c3f 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx @@ -3,9 +3,16 @@ import { type EnvironmentModeEnum, type EnvironmentOverviewResponse } from 'qove import { Suspense, useMemo } from 'react' import { match } from 'ts-pattern' import { ClusterAvatar } from '@qovery/domains/clusters/feature' -import { CreateCloneEnvironmentModal, EnvironmentMode, useEnvironments } from '@qovery/domains/environments/feature' +import { + CreateCloneEnvironmentModal, + EnvironmentMode, + MenuManageDeployment, + MenuOtherActions, + useEnvironments, +} from '@qovery/domains/environments/feature' import { useEnvironmentsOverview, useProject } from '@qovery/domains/projects/feature' import { + ActionToolbar, Button, Heading, Icon, @@ -26,7 +33,7 @@ export const Route = createFileRoute('/_authenticated/organization/$organization component: RouteComponent, }) -const gridLayoutClassName = 'grid grid-cols-[3fr_2fr_2fr_180px_100px]' +const gridLayoutClassName = 'grid grid-cols-[3fr_2fr_2fr_180px_106px]' function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) { const { organizationId, projectId } = useParams({ strict: false }) @@ -72,17 +79,31 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) { {overview.cluster && ( -
+ {overview.cluster?.name} -
+ )}
{timeAgo(new Date(overview.updated_at ?? Date.now()))} ago
-
+
+ {environment && overview.deployment_status && overview.service_count > 0 && ( + + + + + )} +
) diff --git a/libs/domains/environments/feature/src/lib/environment-action-toolbar/environment-action-toolbar.tsx b/libs/domains/environments/feature/src/lib/environment-action-toolbar/environment-action-toolbar.tsx index cb6b6a9cb45..efacf243517 100644 --- a/libs/domains/environments/feature/src/lib/environment-action-toolbar/environment-action-toolbar.tsx +++ b/libs/domains/environments/feature/src/lib/environment-action-toolbar/environment-action-toolbar.tsx @@ -39,7 +39,7 @@ import { UpdateAllModal } from '../update-all-modal/update-all-modal' type ActionToolbarVariant = 'default' | 'deployment' -function MenuManageDeployment({ +export function MenuManageDeployment({ environment, deploymentStatus, variant, @@ -163,15 +163,14 @@ function MenuManageDeployment({
{match(state) .with('DEPLOYING', 'RESTARTING', 'BUILDING', 'DELETING', 'CANCELING', 'STOPPING', () => ( - + )) .with('DEPLOYMENT_QUEUED', 'DELETE_QUEUED', 'STOP_QUEUED', 'RESTART_QUEUED', () => ( - + )) .otherwise(() => ( - + ))} -
@@ -243,7 +242,7 @@ function MenuManageDeployment({ ) } -function MenuOtherActions({ state, environment }: { state: StateEnum; environment: Environment }) { +export function MenuOtherActions({ state, environment }: { state: StateEnum; environment: Environment }) { const { openModal, closeModal } = useModal() const { openModalConfirmation } = useModalConfirmation() const { mutate: deleteEnvironment } = useDeleteEnvironment({ projectId: environment.project.id }) diff --git a/libs/domains/environments/feature/src/lib/hooks/use-cancel-deployment-environment/use-cancel-deployment-environment.ts b/libs/domains/environments/feature/src/lib/hooks/use-cancel-deployment-environment/use-cancel-deployment-environment.ts index 6e298682de3..df0e67ec633 100644 --- a/libs/domains/environments/feature/src/lib/hooks/use-cancel-deployment-environment/use-cancel-deployment-environment.ts +++ b/libs/domains/environments/feature/src/lib/hooks/use-cancel-deployment-environment/use-cancel-deployment-environment.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@tanstack/react-router' import { mutations } from '@qovery/domains/environments/data-access' import { queries } from '@qovery/state/util-queries' @@ -26,7 +26,7 @@ export function useCancelDeploymentEnvironment({ projectId, logsLink }: { projec ...(logsLink ? { labelAction: 'See deployment logs', - callback: () => navigate(logsLink), + callback: () => navigate({ to: logsLink }), } : {}), }, diff --git a/libs/domains/environments/feature/src/lib/hooks/use-delete-environment/use-delete-environment.ts b/libs/domains/environments/feature/src/lib/hooks/use-delete-environment/use-delete-environment.ts index 88819dd6b68..85072fd99fc 100644 --- a/libs/domains/environments/feature/src/lib/hooks/use-delete-environment/use-delete-environment.ts +++ b/libs/domains/environments/feature/src/lib/hooks/use-delete-environment/use-delete-environment.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@tanstack/react-router' import { mutations } from '@qovery/domains/environments/data-access' import { queries } from '@qovery/state/util-queries' @@ -22,7 +22,7 @@ export function useDeleteEnvironment({ projectId, logsLink }: { projectId: strin ...(logsLink ? { labelAction: 'See deployment logs', - callback: () => navigate(logsLink), + callback: () => navigate({ to: logsLink }), } : {}), }, diff --git a/libs/domains/environments/feature/src/lib/hooks/use-deploy-environment/use-deploy-environment.ts b/libs/domains/environments/feature/src/lib/hooks/use-deploy-environment/use-deploy-environment.ts index e36af1a2469..bc528ccbd24 100644 --- a/libs/domains/environments/feature/src/lib/hooks/use-deploy-environment/use-deploy-environment.ts +++ b/libs/domains/environments/feature/src/lib/hooks/use-deploy-environment/use-deploy-environment.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@tanstack/react-router' import { mutations } from '@qovery/domains/environments/data-access' import { queries } from '@qovery/state/util-queries' @@ -26,7 +26,7 @@ export function useDeployEnvironment({ projectId, logsLink }: { projectId: strin ...(logsLink ? { labelAction: 'See deployment logs', - callback: () => navigate(logsLink), + callback: () => navigate({ to: logsLink }), } : {}), }, diff --git a/libs/domains/environments/feature/src/lib/hooks/use-stop-environment/use-stop-environment.ts b/libs/domains/environments/feature/src/lib/hooks/use-stop-environment/use-stop-environment.ts index 359d515d0ad..67b2ad2692d 100644 --- a/libs/domains/environments/feature/src/lib/hooks/use-stop-environment/use-stop-environment.ts +++ b/libs/domains/environments/feature/src/lib/hooks/use-stop-environment/use-stop-environment.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@tanstack/react-router' import { mutations } from '@qovery/domains/environments/data-access' import { queries } from '@qovery/state/util-queries' @@ -26,7 +26,7 @@ export function useStopEnvironment({ projectId, logsLink }: { projectId: string; ...(logsLink ? { labelAction: 'See deployment logs', - callback: () => navigate(logsLink), + callback: () => navigate({ to: logsLink }), } : {}), }, diff --git a/libs/domains/environments/feature/src/lib/hooks/use-uninstall-environment/use-uninstall-environment.ts b/libs/domains/environments/feature/src/lib/hooks/use-uninstall-environment/use-uninstall-environment.ts index 3de23d9d943..d7ed7a345d3 100644 --- a/libs/domains/environments/feature/src/lib/hooks/use-uninstall-environment/use-uninstall-environment.ts +++ b/libs/domains/environments/feature/src/lib/hooks/use-uninstall-environment/use-uninstall-environment.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@tanstack/react-router' import { mutations } from '@qovery/domains/environments/data-access' import { queries } from '@qovery/state/util-queries' @@ -26,7 +26,7 @@ export function useUninstallEnvironment({ projectId, logsLink }: { projectId: st ...(logsLink ? { labelAction: 'See deployment logs', - callback: () => navigate(logsLink), + callback: () => navigate({ to: logsLink }), } : {}), }, From 8cad5d5bbbf7ba50677022a5b7b4a1591ce4e35c Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Fri, 23 Jan 2026 17:53:34 +0100 Subject: [PATCH 8/8] impr: add new icons and statuses for "last operation" --- .../project/$projectId/overview.tsx | 7 +- libs/shared/ui/src/index.ts | 1 + .../deployment-action/deployment-action.tsx | 189 ++++++++++++++++++ 3 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 libs/shared/ui/src/lib/components/deployment-action/deployment-action.tsx diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx index c47f76f4c3f..18d944bcc0f 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx @@ -14,6 +14,7 @@ import { useEnvironmentsOverview, useProject } from '@qovery/domains/projects/fe import { ActionToolbar, Button, + DeploymentAction, Heading, Icon, LoaderSpinner, @@ -25,7 +26,7 @@ import { useModal, } from '@qovery/shared/ui' import { timeAgo } from '@qovery/shared/util-dates' -import { pluralize, twMerge, upperCaseFirstLetter } from '@qovery/shared/util-js' +import { pluralize, twMerge } from '@qovery/shared/util-js' const { Table } = TablePrimitives @@ -67,9 +68,7 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
- - {upperCaseFirstLetter(overview.deployment_status?.last_deployment_state.replace('_', ' '))} - + {timeAgo(new Date(overview.deployment_status?.last_deployment_date ?? Date.now()))} ago diff --git a/libs/shared/ui/src/index.ts b/libs/shared/ui/src/index.ts index 081ddbd1ad6..e8c9d0a168f 100644 --- a/libs/shared/ui/src/index.ts +++ b/libs/shared/ui/src/index.ts @@ -119,3 +119,4 @@ export * from './lib/components/sidebar/sidebar' export * from './lib/utils/toast' export * from './lib/utils/toast-error' export * from './lib/utils/ansi' +export * from './lib/components/deployment-action/deployment-action' diff --git a/libs/shared/ui/src/lib/components/deployment-action/deployment-action.tsx b/libs/shared/ui/src/lib/components/deployment-action/deployment-action.tsx new file mode 100644 index 00000000000..8fdd0f90396 --- /dev/null +++ b/libs/shared/ui/src/lib/components/deployment-action/deployment-action.tsx @@ -0,0 +1,189 @@ +import { StateEnum } from 'qovery-typescript-axios' +import { forwardRef } from 'react' +import { match } from 'ts-pattern' +import { twMerge } from '@qovery/shared/util-js' +import { type IconSVGProps } from '../icon/icon' + +export const DeployIcon = forwardRef(function ( + { className = '', ...props }, + forwardedRef +) { + return ( + + + + + + + + + + + ) +}) + +export const RestartIcon = forwardRef(function ( + { className = '', ...props }, + forwardedRef +) { + return ( + + + + + + + + + + + ) +}) + +export const DeleteIcon = forwardRef(function ( + { className = '', ...props }, + forwardedRef +) { + return ( + + + + + + + + + + + ) +}) + +export const StopIcon = forwardRef(function ({ className = '', ...props }, forwardedRef) { + return ( + + + + + + + + + + + ) +}) + +export const getDeploymentAction = (status: StateEnum | undefined) => { + return match(status) + .with( + StateEnum.QUEUED, + StateEnum.WAITING_RUNNING, + StateEnum.DEPLOYING, + StateEnum.DEPLOYED, + StateEnum.DEPLOYMENT_ERROR, + StateEnum.DEPLOYMENT_QUEUED, + // Other states categorized as "deploy" + StateEnum.BUILDING, + StateEnum.BUILD_ERROR, + StateEnum.CANCELING, + StateEnum.CANCELED, + StateEnum.EXECUTING, + StateEnum.READY, + StateEnum.RECAP, + undefined, + () => ({ + status: 'Deploy', + icon: , + }) + ) + .with( + StateEnum.RESTARTED, + StateEnum.RESTARTING, + StateEnum.RESTART_ERROR, + StateEnum.RESTART_QUEUED, + StateEnum.WAITING_RESTARTING, + () => ({ + status: 'Restart', + icon: , + }) + ) + .with( + StateEnum.DELETED, + StateEnum.DELETE_ERROR, + StateEnum.DELETE_QUEUED, + StateEnum.DELETING, + StateEnum.WAITING_DELETING, + () => ({ + status: 'Delete', + icon: , + }) + ) + .with( + StateEnum.STOPPED, + StateEnum.STOPPING, + StateEnum.STOP_ERROR, + StateEnum.STOP_QUEUED, + StateEnum.WAITING_STOPPING, + () => ({ + status: 'Stop', + icon: , + }) + ) + .exhaustive() +} + +export const DeploymentAction = ({ status }: { status: StateEnum | undefined }) => { + const action = getDeploymentAction(status) + if (!status || !action) return null + + return ( +
+ {action.icon} + {action.status} +
+ ) +}
+ {children}