feat: app deployment UI (#5088)

This commit is contained in:
Laurin Quast 2024-07-19 11:35:37 +02:00 committed by GitHub
parent 75ad8dfa9d
commit e720773208
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 771 additions and 14 deletions

View file

@ -22,6 +22,7 @@ export enum Page {
History = 'history',
Insights = 'insights',
Laboratory = 'laboratory',
Apps = 'apps',
Settings = 'settings',
}
@ -36,6 +37,7 @@ const TargetLayoutQuery = graphql(`
id
cleanId
name
isAppDeploymentsEnabled
me {
id
...CanAccessTarget_MemberFragment
@ -210,6 +212,20 @@ export const TargetLayout = ({
Insights
</Link>
</Tabs.Trigger>
{currentOrganization.isAppDeploymentsEnabled && (
<Tabs.Trigger value={Page.Apps} asChild>
<Link
to="/$organizationId/$projectId/$targetId/apps"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
}}
>
Apps
</Link>
</Tabs.Trigger>
)}
<Tabs.Trigger value={Page.Laboratory} asChild>
<Link
to="/$organizationId/$projectId/$targetId/laboratory"

View file

@ -430,7 +430,7 @@ export function OrganizationInvitations(props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="Pending invitations"
subPageTitle="Pending invitations"
description="Active invitations to join this organization. Invitations expire after 7 days."
>
<MemberInvitationButton

View file

@ -528,7 +528,7 @@ export function OrganizationMembers(props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="List of organization members"
subPageTitle="List of organization members"
description="Manage the members of your organization and their permissions."
>
<MemberInvitationButton

View file

@ -852,7 +852,7 @@ export function OrganizationMemberRolesMigration(props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="Migration Wizard"
subPageTitle="Migration Wizard"
description={
<>
<CardDescription>

View file

@ -896,7 +896,7 @@ export function OrganizationMemberRoles(props: {
</AlertDialog>
<SubPageLayout>
<SubPageLayoutHeader
title="List of roles"
subPageTitle="List of roles"
description="Manage the roles that can be assigned to members of this organization."
>
<OrganizationMemberRoleCreateButton

View file

@ -370,7 +370,7 @@ export function CDNAccessTokens(props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="CDN Access Token"
subPageTitle="CDN Access Token"
description={
<>
<CardDescription>

View file

@ -171,7 +171,7 @@ export function SchemaContracts(props: {
<>
<SubPageLayout>
<SubPageLayoutHeader
title="Schema Contracts"
subPageTitle="Schema Contracts"
description={
<>
<CardDescription>

View file

@ -51,14 +51,14 @@ SubPageLayout.displayName = 'SubPageLayout';
type SubPageLayoutHeaderProps = {
children?: ReactNode;
title?: string;
subPageTitle?: ReactNode;
description?: string | ReactNode;
} & HTMLAttributes<HTMLDivElement>;
const SubPageLayoutHeader = forwardRef<HTMLDivElement, SubPageLayoutHeaderProps>((props, ref) => (
<div className="flex flex-row items-center justify-between" ref={ref}>
<div className="space-y-1.5">
<CardTitle>{props.title}</CardTitle>
<CardTitle>{props.subPageTitle}</CardTitle>
{typeof props.description === 'string' ? (
<CardDescription>{props.description}</CardDescription>
) : (

View file

@ -0,0 +1,7 @@
import { cn } from '@/lib/utils';
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('bg-primary/10 animate-pulse rounded-md', className)} {...props} />;
}
export { Skeleton };

View file

@ -5,6 +5,7 @@ import { env } from '@/env/frontend';
import schema from '@/gql/schema';
import { authExchange } from '@urql/exchange-auth';
import { cacheExchange } from '@urql/exchange-graphcache';
import { relayPagination } from '@urql/exchange-graphcache/extras';
import { persistedExchange } from '@urql/exchange-persisted';
import { Mutation } from './urql-cache';
import { networkStatusExchange } from './urql-exchanges/state';
@ -34,6 +35,14 @@ export const urqlClient = createClient({
updates: {
Mutation,
},
resolvers: {
Target: {
appDeployments: relayPagination(),
},
AppDeployment: {
documents: relayPagination(),
},
},
keys: {
RequestsOverTime: noKey,
FailuresOverTime: noKey,

View file

@ -0,0 +1,349 @@
import { useEffect, useState } from 'react';
import ghost from '../../public/images/figures/ghost.svg?url';
import { LoaderCircleIcon } from 'lucide-react';
import { useClient, useQuery } from 'urql';
import { Page, TargetLayout } from '@/components/layouts/target';
import { Button } from '@/components/ui/button';
import { CardDescription } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { EmptyList } from '@/components/ui/empty-list';
import { Meta } from '@/components/ui/meta';
import { SubPageLayoutHeader } from '@/components/ui/page-content-layout';
import { Skeleton } from '@/components/ui/skeleton';
import { Spinner } from '@/components/ui/spinner';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { graphql } from '@/gql';
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
import { Link, useRouter } from '@tanstack/react-router';
const TargetAppsVersionQuery = graphql(`
query TargetAppsVersionQuery(
$organizationId: ID!
$projectId: ID!
$targetId: ID!
$appName: String!
$appVersion: String!
$first: Int
$after: String
) {
organization(selector: { organization: $organizationId }) {
organization {
id
isAppDeploymentsEnabled
}
}
target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) {
id
appDeployment(appName: $appName, appVersion: $appVersion) {
id
name
version
documents(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
hash
body
operationName
insightsHash
}
}
}
}
}
}
`);
export function TargetAppVersionPage(props: {
organizationId: string;
projectId: string;
targetId: string;
appName: string;
appVersion: string;
}) {
const [data] = useQuery({
query: TargetAppsVersionQuery,
variables: {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
appName: props.appName,
appVersion: props.appVersion,
first: 20,
after: null,
},
});
const router = useRouter();
const client = useClient();
const [isLoadingMore, setIsLoadingMore] = useState(false);
const isAppDeploymentsEnabled =
!data.fetching && !data.stale && !data.data?.organization?.organization.isAppDeploymentsEnabled;
useEffect(() => {
if (isAppDeploymentsEnabled) {
void router.navigate({
to: '/$organizationId/$projectId/$targetId',
params: {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
},
replace: true,
});
}
}, [isAppDeploymentsEnabled]);
const title = data.data?.target?.appDeployment
? `${data.data.target.appDeployment.name}@${data.data.target.appDeployment.version}`
: 'App Deployment';
if (!data.fetching && !data.stale && !data?.data?.target?.appDeployment) {
return (
<>
<Meta title="App Version Not found" />
<TargetLayout
targetId={props.targetId}
projectId={props.projectId}
organizationId={props.organizationId}
page={Page.Apps}
className="min-h-content"
>
<div className="flex h-full flex-1 flex-col items-center justify-center gap-2.5 py-6">
<img
src={ghost}
alt="Ghost illustration"
width="200"
height="200"
className="drag-none"
/>
<h2 className="text-xl font-bold">App Version not found.</h2>
<h3 className="font-semibold">This app does not seem to exist anymore.</h3>
<Button variant="secondary" className="mt-2" onClick={router.history.back}>
Go back
</Button>
</div>
</TargetLayout>
</>
);
}
return (
<>
<Meta title={title} />
<TargetLayout
targetId={props.targetId}
projectId={props.projectId}
organizationId={props.organizationId}
page={Page.Apps}
className="min-h-content"
>
<div className="flex h-full flex-1 flex-col py-6">
<SubPageLayoutHeader
subPageTitle={
<span className="flex items-center">
<Link
to="/$organizationId/$projectId/$targetId/apps"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
}}
>
App Deployments
</Link>{' '}
<span className="inline-block px-2 italic text-gray-500">/</span>{' '}
{data.data?.target?.appDeployment ? (
`${data.data.target.appDeployment.name}@${data.data.target.appDeployment.version}`
) : (
<Skeleton className="inline-block h-5 w-[150px]" />
)}
</span>
}
description={
<>
<CardDescription>
Group your GraphQL operations by app version for app version statistics and
persisted operations.
</CardDescription>
{/* <CardDescription>
<DocsLink
href="/management/targets#cdn-access-tokens"
className="text-gray-500 hover:text-gray-300"
>
Learn more about App Deployments
</DocsLink>
</CardDescription> */}
</>
}
/>
<div className="mt-4" />
{data.fetching || data.stale ? (
<div className="flex h-fit flex-1 items-center justify-center">
<div className="flex flex-col items-center">
<Spinner />
<div className="mt-2 text-xs">Loading app deployments</div>
</div>
</div>
) : !data.data?.target?.appDeployment?.documents?.edges.length ? (
<EmptyList
title="No documents have been uploaded for this app deployment"
description="You can upload documents via the Hive CLI"
docsUrl="/features/schema-registry#app-deplyments"
/>
) : (
<>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="hidden sm:table-cell">Document Hash</TableHead>
<TableHead className="hidden sm:table-cell">Operation Name</TableHead>
<TableHead className="hidden text-end sm:table-cell">
Document Content
</TableHead>
<TableHead className="hidden sm:table-cell" />
</TableRow>
</TableHeader>
<TableBody>
{data.data?.target?.appDeployment.documents?.edges.map(edge => (
<TableRow>
<TableCell>
<span className="rounded bg-gray-800 p-1 font-mono text-sm">
{edge.node.hash}
</span>
</TableCell>
<TableCell>
{!edge.node.operationName ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-help italic">anonymous</span>
</TooltipTrigger>
<TooltipContent>
<p>The operation within the document has no name.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span className="rounded bg-gray-800 p-1 font-mono text-xs">
{edge.node.operationName}
</span>
)}
</TableCell>
<TableCell className="text-end">
<span className="rounded bg-gray-800 p-1 font-mono text-xs">
{edge.node.body.length > 43
? edge.node.body.substring(0, 43).replace(/\n/g, '\\n') + '...'
: edge.node.body}
</span>
</TableCell>
<TableCell className="text-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon-sm" variant="ghost">
<DotsHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem asChild className="cursor-pointer">
<Link
to="/$organizationId/$projectId/$targetId/laboratory"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
}}
search={{
operationString: edge.node.body,
}}
>
Open in Laboratory
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link
to="/$organizationId/$projectId/$targetId/insights/$operationName/$operationHash"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
operationName: edge.node.operationName ?? edge.node.hash,
operationHash: edge.node.insightsHash,
}}
>
Show Insights
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="mt-2">
<Button
size="sm"
variant="outline"
className="ml-auto mr-0 flex"
disabled={
!data?.data?.target?.appDeployment?.documents?.pageInfo?.hasNextPage ||
isLoadingMore
}
onClick={() => {
if (
data?.data?.target?.appDeployment?.documents?.pageInfo?.endCursor &&
data?.data?.target?.appDeployment?.documents?.pageInfo?.hasNextPage
) {
setIsLoadingMore(true);
void client
.query(TargetAppsVersionQuery, {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
appName: props.appName,
appVersion: props.appVersion,
first: 20,
after: data?.data?.target?.appDeployment?.documents.pageInfo?.endCursor,
})
.toPromise()
.finally(() => {
setIsLoadingMore(false);
});
}
}}
>
{isLoadingMore ? (
<>
<LoaderCircleIcon className="mr-2 inline size-4 animate-spin" /> Loading
</>
) : (
'Load more'
)}
</Button>
</div>
</>
)}
</div>
</TargetLayout>
</>
);
}

View file

@ -0,0 +1,326 @@
import { useEffect, useState } from 'react';
import { format } from 'date-fns';
import { LoaderCircleIcon } from 'lucide-react';
import { useClient, useQuery } from 'urql';
import { Page, TargetLayout } from '@/components/layouts/target';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { CardDescription } from '@/components/ui/card';
import { EmptyList, noSchemaVersion } from '@/components/ui/empty-list';
import { Meta } from '@/components/ui/meta';
import { SubPageLayoutHeader } from '@/components/ui/page-content-layout';
import { Spinner } from '@/components/ui/spinner';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { TimeAgo } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { Link, useRouter } from '@tanstack/react-router';
const AppTableRow_AppDeploymentFragment = graphql(`
fragment AppTableRow_AppDeploymentFragment on AppDeployment {
id
name
version
status
totalDocumentCount
lastUsed
}
`);
const TargetAppsViewQuery = graphql(`
query TargetAppsViewQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!, $after: String) {
organization(selector: { organization: $organizationId }) {
organization {
id
isAppDeploymentsEnabled
}
}
target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) {
id
latestSchemaVersion {
id
__typename
}
appDeployments(first: 20, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
...AppTableRow_AppDeploymentFragment
}
}
}
}
}
`);
const TargetAppsViewFetchMoreQuery = graphql(`
query TargetAppsViewFetchMoreQuery(
$organizationId: ID!
$projectId: ID!
$targetId: ID!
$after: String!
) {
target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) {
id
appDeployments(first: 20, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
...AppTableRow_AppDeploymentFragment
}
}
}
}
}
`);
function AppTableRow(props: {
organizationId: string;
projectId: string;
targetId: string;
appDeployment: FragmentType<typeof AppTableRow_AppDeploymentFragment>;
}) {
const appDeployment = useFragment(AppTableRow_AppDeploymentFragment, props.appDeployment);
return (
<TableRow>
<TableCell>
<Link
className="font-mono text-xs font-bold"
to="/$organizationId/$projectId/$targetId/apps/$appName/$appVersion"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
appName: appDeployment.name,
appVersion: appDeployment.version,
}}
>
{appDeployment.name}@{appDeployment.version}
</Link>
</TableCell>
<TableCell className="hidden text-center sm:table-cell">
<Badge className="text-xs" variant="secondary">
{appDeployment.status}
</Badge>
</TableCell>
<TableCell className="text-center">{appDeployment.totalDocumentCount}</TableCell>
<TableCell className="text-end">
{appDeployment.lastUsed ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TimeAgo date={appDeployment.lastUsed} className="cursor-help text-xs" />{' '}
</TooltipTrigger>
<TooltipContent>
<p>
{'Last operation reported on '}
{format(appDeployment.lastUsed, 'dd.MM.yyyy')}
{' at '}
{format(appDeployment.lastUsed, 'HH:mm')}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge className="cursor-help text-xs" variant="outline">
No data
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>There was no usage reported yet.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</TableCell>
</TableRow>
);
}
function TargetAppsView(props: { organizationId: string; projectId: string; targetId: string }) {
const [data] = useQuery({
query: TargetAppsViewQuery,
variables: {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
},
});
const client = useClient();
const router = useRouter();
const [isLoadingMore, setIsLoadingMore] = useState(false);
const isAppDeploymentsEnabled =
!data.fetching && !data.stale && !data.data?.organization?.organization.isAppDeploymentsEnabled;
useEffect(() => {
if (isAppDeploymentsEnabled) {
void router.navigate({
to: '/$organizationId/$projectId/$targetId',
params: {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
},
replace: true,
});
}
}, [isAppDeploymentsEnabled]);
return (
<div className="flex flex-1 flex-col py-6">
<SubPageLayoutHeader
subPageTitle="App Deployments"
description={
<>
<CardDescription>
Group your GraphQL operations by app version for app version statistics and persisted
operations.
</CardDescription>
{/* <CardDescription>
<DocsLink
href="/management/app-deployments"
className="text-gray-500 hover:text-gray-300"
>
Learn more about App Deployments
</DocsLink>
</CardDescription> */}
</>
}
/>
<div className="mt-4" />
{data.fetching || data.stale ? (
<div className="flex h-fit flex-1 items-center justify-center">
<div className="flex flex-col items-center">
<Spinner />
<div className="mt-2 text-xs">Loading app deployments</div>
</div>
</div>
) : !data.data?.target?.latestSchemaVersion ? (
noSchemaVersion
) : !data.data.target.appDeployments ? (
<EmptyList
title="Hive is waiting for your first app deployment"
description="You can create an app deployment with the Hive CLI"
docsUrl="/features/schema-registry#app-deplyments"
/>
) : (
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="hidden sm:table-cell">App@Version</TableHead>
<TableHead className="hidden text-center sm:table-cell">Status</TableHead>
<TableHead className="hidden text-center sm:table-cell">
Amount of Documents
</TableHead>
<TableHead className="hidden text-end sm:table-cell">
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="cursor-help">Last used</TooltipTrigger>
<TooltipContent className="max-w-64 text-start">
Last time a request was sent for this app. Requires usage reporting being
set up.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.data?.target?.appDeployments?.edges.map(edge => (
<AppTableRow
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
appDeployment={edge.node}
/>
))}
</TableBody>
</Table>
</div>
<div className="mt-2">
<Button
size="sm"
variant="outline"
className="ml-auto mr-0 flex"
disabled={!data?.data?.target?.appDeployments?.pageInfo?.hasNextPage || isLoadingMore}
onClick={() => {
if (
data?.data?.target?.appDeployments?.pageInfo?.endCursor &&
data?.data?.target?.appDeployments?.pageInfo?.hasNextPage
) {
setIsLoadingMore(true);
void client
.query(TargetAppsViewFetchMoreQuery, {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
after: data?.data?.target?.appDeployments?.pageInfo?.endCursor,
})
.toPromise()
.finally(() => {
setIsLoadingMore(false);
});
}
}}
>
{isLoadingMore ? (
<>
<LoaderCircleIcon className="mr-2 inline size-4 animate-spin" /> Loading
</>
) : (
'Load more'
)}
</Button>
</div>
</div>
)}
</div>
);
}
export function TargetAppsPage(props: {
organizationId: string;
projectId: string;
targetId: string;
}) {
return (
<>
<Meta title="App Deployments" />
<TargetLayout
targetId={props.targetId}
projectId={props.projectId}
organizationId={props.organizationId}
page={Page.Apps}
>
<TargetAppsView
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
/>
</TargetLayout>
</>
);
}

View file

@ -362,6 +362,20 @@ function useOperationCollectionsPlugin(props: {
useEffect(() => {
if (!hasAllEditors || !currentOperation) {
const searchObj = router.latestLocation.search;
const operationString =
'operationString' in searchObj && typeof searchObj.operationString === 'string'
? searchObj.operationString
: null;
// We provide an operation string when navigating to the laboratory from persisted documents
// in that case we want to show that operation within this tab.
if (operationString) {
editorContext.queryEditor?.setValue(operationString);
editorContext.variableEditor?.setValue('');
editorContext.headerEditor?.setValue('');
}
return;
}

View file

@ -139,7 +139,7 @@ function RegistryAccessTokens(props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="Registry Access Tokens"
subPageTitle="Registry Access Tokens"
description={
<>
<CardDescription>
@ -244,7 +244,7 @@ const ExtendBaseSchema = (props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="Extend Your Schema"
subPageTitle="Extend Your Schema"
description={
<>
<CardDescription>
@ -538,7 +538,7 @@ const ConditionalBreakingChanges = (props: {
<form onSubmit={handleSubmit}>
<SubPageLayout>
<SubPageLayoutHeader
title="Conditional Breaking Changes"
subPageTitle="Conditional Breaking Changes"
description={
<>
<CardDescription>
@ -784,7 +784,7 @@ function TargetName(props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="Target Name"
subPageTitle="Target Name"
description={
<>
<CardDescription>
@ -901,7 +901,7 @@ function GraphQLEndpointUrl(props: {
return (
<SubPageLayout>
<SubPageLayoutHeader
title="GraphQL Endpoint URL"
subPageTitle="GraphQL Endpoint URL"
description={
<>
<CardDescription>
@ -1003,7 +1003,7 @@ function TargetDelete(props: { organizationId: string; projectId: string; target
return (
<SubPageLayout>
<SubPageLayoutHeader
title="Delete Target"
subPageTitle="Delete Target"
description={
<>
<CardDescription>

View file

@ -53,6 +53,8 @@ import { ProjectAlertsPage } from './pages/project-alerts';
import { ProjectPolicyPage } from './pages/project-policy';
import { ProjectSettingsPage } from './pages/project-settings';
import { TargetPage } from './pages/target';
import { TargetAppVersionPage } from './pages/target-app-version';
import { TargetAppsPage } from './pages/target-apps';
import { TargetChecksPage } from './pages/target-checks';
import { TargetChecksSinglePage } from './pages/target-checks-single';
import { TargetExplorerPage } from './pages/target-explorer';
@ -549,6 +551,35 @@ const targetLaboratoryRoute = createRoute({
},
});
const targetAppsRoute = createRoute({
getParentRoute: () => targetRoute,
path: 'apps',
component: function TargetAppsRoute() {
const { organizationId, projectId, targetId } = targetAppsRoute.useParams();
return (
<TargetAppsPage organizationId={organizationId} projectId={projectId} targetId={targetId} />
);
},
});
const targetAppVersionRoute = createRoute({
getParentRoute: () => targetRoute,
path: 'apps/$appName/$appVersion',
component: function TargetAppVersionRoute() {
const { organizationId, projectId, targetId, appName, appVersion } =
targetAppVersionRoute.useParams();
return (
<TargetAppVersionPage
organizationId={organizationId}
projectId={projectId}
targetId={targetId}
appName={appName}
appVersion={appVersion}
/>
);
},
});
const targetInsightsRoute = createRoute({
getParentRoute: () => targetRoute,
path: 'insights',
@ -789,6 +820,8 @@ const routeTree = root.addChildren([
targetExplorerUnusedRoute,
targetExplorerTypeRoute,
targetChecksRoute.addChildren([targetChecksSingleRoute]),
targetAppVersionRoute,
targetAppsRoute,
]),
]),
]);

View file

@ -256,6 +256,9 @@ module.exports = {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
minHeight: {
content: 'var(--content-height)',
},
},
},
plugins: [