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..18d944bcc0f --- /dev/null +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/overview.tsx @@ -0,0 +1,263 @@ +import { Link, createFileRoute, useParams } from '@tanstack/react-router' +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 { + CreateCloneEnvironmentModal, + EnvironmentMode, + MenuManageDeployment, + MenuOtherActions, + useEnvironments, +} from '@qovery/domains/environments/feature' +import { useEnvironmentsOverview, useProject } from '@qovery/domains/projects/feature' +import { + ActionToolbar, + Button, + DeploymentAction, + Heading, + Icon, + LoaderSpinner, + Section, + StatusChip, + TablePrimitives, + Tooltip, + Truncate, + useModal, +} from '@qovery/shared/ui' +import { timeAgo } from '@qovery/shared/util-dates' +import { pluralize, twMerge } from '@qovery/shared/util-js' + +const { Table } = TablePrimitives + +export const Route = createFileRoute('/_authenticated/organization/$organizationId/project/$projectId/overview')({ + component: RouteComponent, +}) + +const gridLayoutClassName = 'grid grid-cols-[3fr_2fr_2fr_180px_106px]' + +function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) { + const { organizationId, projectId } = useParams({ strict: false }) + const { data: environments = [] } = useEnvironments({ projectId, suspense: true }) + const environment = environments.find((env) => env.id === overview.id) + const runningStatus = environment?.runningStatus + + return ( + + +
+ + + +
+ + {overview.service_count} {pluralize(overview.service_count, 'service')} + + {runningStatus && ( + + + + )} +
+
+
+ +
+
+ + + {timeAgo(new Date(overview.deployment_status?.last_deployment_date ?? Date.now()))} ago + +
+ +
+
+ + {overview.cluster && ( + + + {overview.cluster?.name} + + )} + + +
{timeAgo(new Date(overview.updated_at ?? Date.now()))} ago
+
+ +
+ {environment && overview.deployment_status && overview.service_count > 0 && ( + + + + + )} +
+
+
+ ) +} + +function EnvironmentSection({ + type, + items, + onCreateEnvClicked, +}: { + type: EnvironmentModeEnum + items: EnvironmentOverviewResponse[] + onCreateEnvClicked: () => void +}) { + const title = match(type) + .with('PRODUCTION', () => 'Production') + .with('STAGING', () => 'Staging') + .with('DEVELOPMENT', () => 'Development') + .with('PREVIEW', () => 'Ephemeral') + .exhaustive() + + return ( +
+
+ + {title} +
+ {items.length === 0 ? ( +
+ + No {title.toLowerCase()} environment created yet + +
+ ) : ( +
+ + + + + Environment + + + Last operation + + + Cluster + + + Last update + + + Actions + + + + + + {items.map((environmentOverview) => ( + + ))} + + +
+ )} +
+ ) +} + +function ProjectOverview() { + const { openModal, closeModal } = useModal() + const { organizationId, projectId } = useParams({ strict: false }) + const { data: project } = useProject({ organizationId, projectId, suspense: true }) + const { data: environmentsOverview } = useEnvironmentsOverview({ projectId, suspense: true }) + + const groupedEnvs = useMemo(() => { + return environmentsOverview?.reduce((acc, env) => { + acc.set(env.mode, [...(acc.get(env.mode) || []), env]) + return acc + }, new Map()) + }, [environmentsOverview]) + + const onCreateEnvClicked = () => { + openModal({ + content: ( + + ), + options: { + fakeModal: true, + }, + }) + } + + return ( +
+
+
+
+ {project?.name} + +
+
+
+
+ + + + +
+
+
+ ) +} + +function RouteComponent() { + return ( + + + + } + > + + + ) +} 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/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/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/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/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/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/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-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/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 }), } : {}), }, 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/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..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} +
+ ) +} 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?