mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 17:18:23 +00:00
feat: app deployment UI (#5088)
This commit is contained in:
parent
75ad8dfa9d
commit
e720773208
16 changed files with 771 additions and 14 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -852,7 +852,7 @@ export function OrganizationMemberRolesMigration(props: {
|
|||
return (
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Migration Wizard"
|
||||
subPageTitle="Migration Wizard"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ export function CDNAccessTokens(props: {
|
|||
return (
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="CDN Access Token"
|
||||
subPageTitle="CDN Access Token"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ export function SchemaContracts(props: {
|
|||
<>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Schema Contracts"
|
||||
subPageTitle="Schema Contracts"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
7
packages/web/app/src/components/ui/skeleton.tsx
Normal file
7
packages/web/app/src/components/ui/skeleton.tsx
Normal 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 };
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
349
packages/web/app/src/pages/target-app-version.tsx
Normal file
349
packages/web/app/src/pages/target-app-version.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
326
packages/web/app/src/pages/target-apps.tsx
Normal file
326
packages/web/app/src/pages/target-apps.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue