Skip to content

Commit 06567cd

Browse files
feat(aurora-portal): implement a proper service discovery (#385)
* feat(aurora-portal): add a new action `availableServices` ISSUES CLOSED: #371 * test(aurora-portal): add unit tests for the 'session' router * feat(dashboard): implement routing protection on the 'compute' dashboard level * feat(dashboard): update the \"Compute Dashboard\" ui to handle navigation links * feat(dashboard): implement correct rendering logic of top navigation links * feat(aurora-portal): add utility to check sevice availability
1 parent bd2fde1 commit 06567cd

9 files changed

Lines changed: 862 additions & 47 deletions

File tree

apps/aurora-portal/src/client/routes/_auth/accounts/$accountId/projects/$projectId.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createFileRoute, Outlet } from "@tanstack/react-router"
1+
import { createFileRoute, Outlet, useLoaderData } from "@tanstack/react-router"
22
import { ProjectSubNavigation } from "./-components/ProjectSubNavigation"
33

44
export const Route = createFileRoute("/_auth/accounts/$accountId/projects/$projectId")({
@@ -9,20 +9,24 @@ export const Route = createFileRoute("/_auth/accounts/$accountId/projects/$proje
99
type: "project",
1010
projectId: params.projectId || "",
1111
})
12+
const availableServices = await context.trpcClient?.auth.getAvailableServices.query()
1213

1314
return {
1415
trpcClient: context.trpcClient,
1516
crumbDomain: { path: `/accounts/${params.accountId}/projects`, name: data?.domain?.name },
1617
crumbProject: data?.project,
18+
availableServices,
1719
}
1820
},
1921
})
2022

2123
function RouteComponent() {
24+
const { availableServices } = useLoaderData({ from: Route.id })
25+
2226
return (
2327
<div>
2428
<div className="w-full flex">
25-
<ProjectSubNavigation />
29+
<ProjectSubNavigation availableServices={availableServices!} />
2630
</div>
2731
<div className="py-4 pl-4 h-full">
2832
<Outlet /> {/* This is where child routes will render */}

apps/aurora-portal/src/client/routes/_auth/accounts/$accountId/projects/$projectId/compute/$.tsx

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,57 @@
1-
import { createFileRoute, ErrorComponent, useParams } from "@tanstack/react-router"
1+
import { createFileRoute, ErrorComponent, redirect, useParams } from "@tanstack/react-router"
2+
import { getServiceIndex } from "@/server/Authentication/helpers"
3+
import { TrpcClient } from "@/client/trpcClient"
24
import { ErrorBoundary } from "react-error-boundary"
35
import { ComputeSideNavBar } from "./-components/ComputeNavBar"
46
import { Overview } from "./-components/Overview"
5-
import { TrpcClient } from "@/client/trpcClient"
67
import { Instances } from "./-components/Instances/List"
78
import { Images } from "./-components/Images/List"
89
import { KeyPairs } from "./-components/KeyPairs/List"
910
import { ServerGroups } from "./-components/ServerGroups/List"
1011
import { Flavors } from "./-components/Flavors/List"
1112

13+
const checkServiceAvailability = (
14+
availableServices: {
15+
type: string
16+
name: string
17+
}[],
18+
params: {
19+
accountId: string
20+
projectId: string
21+
_splat?: string | undefined
22+
}
23+
) => {
24+
const { _splat: splat = "", accountId } = params
25+
26+
let shouldNavigateToOverview = false
27+
28+
const serviceIndex = getServiceIndex(availableServices)
29+
30+
// Redirect to the "Projects Overview" page if none of compute services available
31+
if (!serviceIndex["image"] && !serviceIndex["compute"]) {
32+
throw redirect({
33+
to: "/accounts/$accountId/projects",
34+
params: { accountId },
35+
})
36+
}
37+
38+
if (splat === "images" && !serviceIndex["image"]["glance"]) {
39+
shouldNavigateToOverview = true
40+
}
41+
42+
if (["instances", "keypairs", "servergroups", "flavors"].includes(splat) && !serviceIndex["compute"]["nova"]) {
43+
shouldNavigateToOverview = true
44+
}
45+
46+
if (shouldNavigateToOverview) {
47+
// Redirect to the "Compute Services Overview" page if a specific compute service is not available
48+
throw redirect({
49+
to: "/accounts/$accountId/projects/$projectId/compute/$",
50+
params: { ...params, _splat: undefined },
51+
})
52+
}
53+
}
54+
1255
export const Route = createFileRoute("/_auth/accounts/$accountId/projects/$projectId/compute/$")({
1356
component: RouteComponent,
1457
errorComponent: ({ error }) => {
@@ -23,19 +66,36 @@ export const Route = createFileRoute("/_auth/accounts/$accountId/projects/$proje
2366
},
2467
loader: async ({ context }) => {
2568
const { trpcClient } = context
69+
const availableServices = await trpcClient?.auth.getAvailableServices.query()
2670

2771
return {
2872
client: trpcClient,
73+
availableServices,
2974
}
3075
},
76+
beforeLoad: async ({ context, params }) => {
77+
const { trpcClient } = context
78+
const availableServices = await trpcClient?.auth.getAvailableServices.query()
79+
80+
checkServiceAvailability(availableServices!, params)
81+
},
3182
})
3283

3384
function RouteComponent() {
34-
const { client } = Route.useLoaderData()
35-
return <ComputeDashboard client={client!} />
85+
const { client, availableServices } = Route.useLoaderData()
86+
return <ComputeDashboard client={client!} availableServices={availableServices!} />
3687
}
3788

38-
function ComputeDashboard({ client }: { client: TrpcClient }) {
89+
function ComputeDashboard({
90+
client,
91+
availableServices,
92+
}: {
93+
client: TrpcClient
94+
availableServices: {
95+
type: string
96+
name: string
97+
}[]
98+
}) {
3999
const { project, splat } = useParams({
40100
from: "/_auth/accounts/$accountId/projects/$projectId/compute/$",
41101
select: (params) => {
@@ -46,7 +106,7 @@ function ComputeDashboard({ client }: { client: TrpcClient }) {
46106
return (
47107
<div className="container max-w-screen-3xl mx-auto px-6 py-4 grid grid-cols-12 gap-4">
48108
<div className="col-span-2 flex flex-col gap-4">
49-
<ComputeSideNavBar />
109+
<ComputeSideNavBar availableServices={availableServices} />
50110
</div>
51111
<div className="col-span-9 flex flex-col gap-4">
52112
<div className="w-full">

apps/aurora-portal/src/client/routes/_auth/accounts/$accountId/projects/$projectId/compute/-components/ComputeNavBar.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import { getServiceIndex } from "@/server/Authentication/helpers"
12
import { ContentHeading } from "@cloudoperators/juno-ui-components/index"
23
import { Trans } from "@lingui/react/macro"
34
import { useLocation, useParams, Link } from "@tanstack/react-router"
45

5-
export const ComputeSideNavBar = () => {
6+
export const ComputeSideNavBar = ({
7+
availableServices,
8+
}: {
9+
availableServices: {
10+
type: string
11+
name: string
12+
}[]
13+
}) => {
614
const location = useLocation()
715

816
const { projectId, domain } = useParams({
@@ -14,14 +22,24 @@ export const ComputeSideNavBar = () => {
1422

1523
const computeRootPath = `/accounts/${domain}/projects/${projectId}/compute`
1624

17-
const links = [
18-
{ path: computeRootPath, label: <Trans>Overview</Trans> },
19-
{ path: `${computeRootPath}/instances`, label: <Trans>Instances</Trans> },
20-
{ path: `${computeRootPath}/images`, label: <Trans>Images</Trans> },
21-
{ path: `${computeRootPath}/keypairs`, label: <Trans>Key Pairs</Trans> },
22-
{ path: `${computeRootPath}/servergroups`, label: <Trans>Server Groups</Trans> },
23-
{ path: `${computeRootPath}/flavors`, label: <Trans>Flavors</Trans> },
24-
]
25+
const serviceIndex = getServiceIndex(availableServices)
26+
27+
const getComputeNavigationLinks = () => {
28+
return [
29+
{ path: computeRootPath, label: <Trans>Overview</Trans> },
30+
...(serviceIndex["image"]["glance"] ? [{ path: `${computeRootPath}/images`, label: <Trans>Images</Trans> }] : []),
31+
...(serviceIndex["compute"]["nova"]
32+
? [
33+
{ path: `${computeRootPath}/instances`, label: <Trans>Instances</Trans> },
34+
{ path: `${computeRootPath}/keypairs`, label: <Trans>Key Pairs</Trans> },
35+
{ path: `${computeRootPath}/servergroups`, label: <Trans>Server Groups</Trans> },
36+
{ path: `${computeRootPath}/flavors`, label: <Trans>Flavors</Trans> },
37+
]
38+
: []),
39+
]
40+
}
41+
42+
const links = getComputeNavigationLinks()
2543

2644
return (
2745
<div className="w-full flex flex-col items-start px-4">

apps/aurora-portal/src/client/routes/_auth/accounts/$accountId/projects/$projectId/network/index.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
import { createFileRoute } from "@tanstack/react-router"
1+
import { getServiceIndex } from "@/server/Authentication/helpers"
2+
import { createFileRoute, redirect } from "@tanstack/react-router"
23

34
export const Route = createFileRoute("/_auth/accounts/$accountId/projects/$projectId/network/")({
45
component: RouteComponent,
6+
beforeLoad: async ({ context, params }) => {
7+
const { trpcClient } = context
8+
const { accountId } = params
9+
const availableServices = await trpcClient?.auth.getAvailableServices.query()
10+
11+
const serviceIndex = getServiceIndex(availableServices || [])
12+
13+
if (!serviceIndex["network"]) {
14+
throw redirect({
15+
to: "/accounts/$accountId/projects",
16+
params: { accountId },
17+
})
18+
}
19+
},
520
})
621

722
function RouteComponent() {
Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,53 @@
11
import { linkOptions, useParams } from "@tanstack/react-router"
22
import { SubNavigationLayout } from "@/client/components/navigation/SubNavigationLayout"
33

4-
const options = [
5-
linkOptions({
6-
to: "/accounts/$accountId/projects/$projectId/compute/$",
7-
label: "Compute",
8-
params: {
9-
accountId: "accountId",
10-
projectId: "projectId",
11-
},
12-
}),
13-
linkOptions({
14-
to: "/accounts/$accountId/projects/$projectId/gardener/clusters",
15-
label: "Gardener",
16-
params: {
17-
accountId: "accountId",
18-
projectId: "projectId",
19-
},
20-
}),
21-
linkOptions({
22-
to: "/accounts/$accountId/projects/$projectId/network",
23-
label: "Network",
24-
params: {
25-
accountId: "accountId",
26-
projectId: "projectId",
27-
},
28-
}),
29-
]
30-
31-
export function ProjectSubNavigation() {
4+
export function ProjectSubNavigation({
5+
availableServices,
6+
}: {
7+
availableServices: {
8+
type: string
9+
name: string
10+
}[]
11+
}) {
3212
const params = useParams({ from: "/_auth/accounts/$accountId/projects/$projectId" })
13+
14+
const getNavigationOptions = () => [
15+
...(availableServices.find(({ type }) => type === "compute") ||
16+
availableServices.find(({ type }) => type === "image")
17+
? [
18+
linkOptions({
19+
to: "/accounts/$accountId/projects/$projectId/compute/$",
20+
label: "Compute",
21+
params: {
22+
accountId: "accountId",
23+
projectId: "projectId",
24+
},
25+
}),
26+
]
27+
: []),
28+
linkOptions({
29+
to: "/accounts/$accountId/projects/$projectId/gardener/clusters",
30+
label: "Gardener",
31+
params: {
32+
accountId: "accountId",
33+
projectId: "projectId",
34+
},
35+
}),
36+
...(availableServices.find(({ type }) => type === "network")
37+
? [
38+
linkOptions({
39+
to: "/accounts/$accountId/projects/$projectId/network",
40+
label: "Network",
41+
params: {
42+
accountId: "accountId",
43+
projectId: "projectId",
44+
},
45+
}),
46+
]
47+
: []),
48+
]
49+
50+
const options = getNavigationOptions()
51+
3352
return <SubNavigationLayout options={options} params={params} />
3453
}

0 commit comments

Comments
 (0)