mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 17:18:23 +00:00
Add searching and sorting options to the list of projects (#5285)
This commit is contained in:
parent
2ec56a03bc
commit
a62bcb15b6
5 changed files with 415 additions and 109 deletions
|
|
@ -21,7 +21,7 @@ const buttonVariants = cva(
|
|||
default: 'h-10 py-2 px-4',
|
||||
sm: 'h-9 px-3 rounded-md',
|
||||
lg: 'h-11 px-8 rounded-md',
|
||||
icon: 'size-9',
|
||||
icon: 'size-10',
|
||||
'icon-sm': 'size-7',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,15 +2,20 @@ import { ReactElement, useMemo, useRef } from 'react';
|
|||
import { endOfDay, formatISO, startOfDay } from 'date-fns';
|
||||
import * as echarts from 'echarts';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Globe, History } from 'lucide-react';
|
||||
import { Globe, History, MoveDownIcon, MoveUpIcon, SearchIcon } from 'lucide-react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { useQuery } from 'urql';
|
||||
import { z } from 'zod';
|
||||
import { OrganizationLayout, Page } from '@/components/layouts/organization';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { EmptyList } from '@/components/ui/empty-list';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Meta } from '@/components/ui/meta';
|
||||
import { Subtitle, Title } from '@/components/ui/page';
|
||||
import { QueryError } from '@/components/ui/query-error';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { ProjectType } from '@/gql/graphql';
|
||||
|
|
@ -18,7 +23,15 @@ import { subDays } from '@/lib/date-time';
|
|||
import { useFormattedNumber } from '@/lib/hooks';
|
||||
import { pluralize } from '@/lib/utils';
|
||||
import { UTCDate } from '@date-fns/utc';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
|
||||
export const OrganizationIndexRouteSearch = z.object({
|
||||
search: z.string().optional(),
|
||||
sortBy: z.enum(['requests', 'versions', 'name']).optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
});
|
||||
|
||||
type RouteSearchProps = z.infer<typeof OrganizationIndexRouteSearch>;
|
||||
|
||||
const ProjectCard_ProjectFragment = graphql(`
|
||||
fragment ProjectCard_ProjectFragment on Project {
|
||||
|
|
@ -240,13 +253,29 @@ const OrganizationProjectsPageQuery = graphql(`
|
|||
}
|
||||
`);
|
||||
|
||||
function OrganizationPageContent(props: { organizationId: string }) {
|
||||
function OrganizationPageContent(
|
||||
props: {
|
||||
organizationId: string;
|
||||
} & RouteSearchProps,
|
||||
) {
|
||||
const days = 14;
|
||||
const period = useRef<{
|
||||
from: string;
|
||||
to: string;
|
||||
}>();
|
||||
|
||||
// Sort by requests by default
|
||||
const sortKey = props.sortBy ?? 'requests';
|
||||
|
||||
const sortOrder =
|
||||
props.sortOrder === 'asc'
|
||||
? -1
|
||||
: // if the sort order is not set, sort by name in ascending order by default
|
||||
!props.sortOrder && props.sortBy === 'name'
|
||||
? -1
|
||||
: // if the sort order is not set, sort in descending order by default
|
||||
1;
|
||||
|
||||
if (!period.current) {
|
||||
const now = new UTCDate();
|
||||
const from = formatISO(startOfDay(subDays(now, days)));
|
||||
|
|
@ -255,6 +284,8 @@ function OrganizationPageContent(props: { organizationId: string }) {
|
|||
period.current = { from, to };
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [query] = useQuery({
|
||||
query: OrganizationProjectsPageQuery,
|
||||
variables: {
|
||||
|
|
@ -266,13 +297,13 @@ function OrganizationPageContent(props: { organizationId: string }) {
|
|||
});
|
||||
|
||||
const currentOrganization = query.data?.organization?.organization;
|
||||
const projects = query.data?.projects;
|
||||
const projectsConnection = query.data?.projects;
|
||||
|
||||
const highestNumberOfRequests = useMemo(() => {
|
||||
let highest = 10;
|
||||
|
||||
if (projects?.nodes.length) {
|
||||
for (const project of projects.nodes) {
|
||||
if (projectsConnection?.nodes.length) {
|
||||
for (const project of projectsConnection.nodes) {
|
||||
for (const dataPoint of project.requestsOverTime) {
|
||||
if (dataPoint.value > highest) {
|
||||
highest = dataPoint.value;
|
||||
|
|
@ -282,7 +313,40 @@ function OrganizationPageContent(props: { organizationId: string }) {
|
|||
}
|
||||
|
||||
return highest;
|
||||
}, [projects]);
|
||||
}, [projectsConnection]);
|
||||
|
||||
const projects = useMemo(() => {
|
||||
if (!projectsConnection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchPhrase = props.search;
|
||||
const newProjects = searchPhrase
|
||||
? projectsConnection.nodes.filter(project =>
|
||||
project.name.toLowerCase().includes(searchPhrase.toLowerCase()),
|
||||
)
|
||||
: projectsConnection.nodes.slice();
|
||||
|
||||
return newProjects.sort((a, b) => {
|
||||
const diffRequests = b.totalRequests - a.totalRequests;
|
||||
const diffVersions = b.schemaVersionsCount - a.schemaVersionsCount;
|
||||
|
||||
if (sortKey === 'requests' && diffRequests !== 0) {
|
||||
return diffRequests * sortOrder;
|
||||
}
|
||||
|
||||
if (sortKey === 'versions' && diffVersions !== 0) {
|
||||
return diffVersions * sortOrder;
|
||||
}
|
||||
|
||||
if (sortKey === 'name') {
|
||||
return a.name.localeCompare(b.name) * sortOrder * -1;
|
||||
}
|
||||
|
||||
// falls back to sort by name in ascending order
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [projectsConnection, props.search, sortKey, sortOrder]);
|
||||
|
||||
if (query.error) {
|
||||
return <QueryError organizationId={props.organizationId} error={query.error} />;
|
||||
|
|
@ -296,12 +360,98 @@ function OrganizationPageContent(props: { organizationId: string }) {
|
|||
>
|
||||
<>
|
||||
<div className="grow">
|
||||
<div className="py-6">
|
||||
<Title>Projects</Title>
|
||||
<Subtitle>A list of available project in your organization.</Subtitle>
|
||||
<div className="flex flex-row items-center justify-between py-6">
|
||||
<div>
|
||||
<Title>Projects</Title>
|
||||
<Subtitle>A list of available project in your organization.</Subtitle>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<div className="relative">
|
||||
<SearchIcon className="text-muted-foreground absolute left-2.5 top-2.5 size-4" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
value={props.search}
|
||||
onChange={event => {
|
||||
void router.navigate({
|
||||
search(params) {
|
||||
return {
|
||||
...params,
|
||||
search: event.target.value,
|
||||
};
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="bg-background w-full rounded-lg pl-8 md:w-[200px] lg:w-[336px]"
|
||||
/>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-4 h-8" />
|
||||
<Select
|
||||
value={props.sortBy ?? 'requests'}
|
||||
onValueChange={value => {
|
||||
void router.navigate({
|
||||
search(params) {
|
||||
return {
|
||||
...params,
|
||||
sortBy: value,
|
||||
};
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="hover:bg-accent bg-transparent">
|
||||
{props.sortBy === 'versions'
|
||||
? 'Schema Versions'
|
||||
: props.sortBy === 'name'
|
||||
? 'Name'
|
||||
: 'Requests'}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="requests">
|
||||
<div className="font-bold">Requests</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
GraphQL requests made in the last {days} days.
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="versions">
|
||||
<div className="font-bold">Schema Versions</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Schemas published in last {days} days.
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="name">
|
||||
<div className="font-bold">Name</div>
|
||||
<div className="text-muted-foreground text-xs">Sort by project name.</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
className="shrink-0"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
void router.navigate({
|
||||
search(params) {
|
||||
return {
|
||||
...params,
|
||||
sortOrder: props.sortOrder === 'asc' ? 'desc' : 'asc',
|
||||
};
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{props.sortOrder === 'asc' ? (
|
||||
<MoveUpIcon className="size-4" />
|
||||
) : (
|
||||
<MoveDownIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentOrganization && projects ? (
|
||||
projects.total === 0 ? (
|
||||
{currentOrganization && projectsConnection ? (
|
||||
projectsConnection.total === 0 ? (
|
||||
<EmptyList
|
||||
title="Hive is waiting for your first project"
|
||||
description='You can create a project by clicking the "Create Project" button'
|
||||
|
|
@ -309,31 +459,17 @@ function OrganizationPageContent(props: { organizationId: string }) {
|
|||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 items-stretch gap-5 xl:grid-cols-3">
|
||||
{projects.nodes
|
||||
.sort((a, b) => {
|
||||
const diffOperations = b.totalRequests - a.totalRequests;
|
||||
if (diffOperations !== 0) {
|
||||
return diffOperations;
|
||||
}
|
||||
|
||||
const diffVersions = b.schemaVersionsCount - a.schemaVersionsCount;
|
||||
if (diffVersions !== 0) {
|
||||
return diffVersions;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map(project => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
cleanOrganizationId={currentOrganization.cleanId}
|
||||
days={days}
|
||||
highestNumberOfRequests={highestNumberOfRequests}
|
||||
project={project}
|
||||
requestsOverTime={project.requestsOverTime}
|
||||
schemaVersionsCount={project.schemaVersionsCount}
|
||||
/>
|
||||
))}
|
||||
{projects.map(project => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
cleanOrganizationId={currentOrganization.cleanId}
|
||||
days={days}
|
||||
highestNumberOfRequests={highestNumberOfRequests}
|
||||
project={project}
|
||||
requestsOverTime={project.requestsOverTime}
|
||||
schemaVersionsCount={project.schemaVersionsCount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
|
|
@ -357,11 +493,20 @@ function OrganizationPageContent(props: { organizationId: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function OrganizationPage(props: { organizationId: string }): ReactElement {
|
||||
export function OrganizationPage(
|
||||
props: {
|
||||
organizationId: string;
|
||||
} & RouteSearchProps,
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Organization" />
|
||||
<OrganizationPageContent organizationId={props.organizationId} />
|
||||
<OrganizationPageContent
|
||||
organizationId={props.organizationId}
|
||||
search={props.search}
|
||||
sortBy={props.sortBy}
|
||||
sortOrder={props.sortOrder}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,19 @@ import { ReactElement, useMemo, useRef } from 'react';
|
|||
import { endOfDay, formatISO, startOfDay } from 'date-fns';
|
||||
import * as echarts from 'echarts';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Globe, History } from 'lucide-react';
|
||||
import { Globe, History, MoveDownIcon, MoveUpIcon, SearchIcon } from 'lucide-react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { useQuery } from 'urql';
|
||||
import { z } from 'zod';
|
||||
import { Page, ProjectLayout } from '@/components/layouts/project';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { EmptyList } from '@/components/ui/empty-list';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Meta } from '@/components/ui/meta';
|
||||
import { Subtitle, Title } from '@/components/ui/page';
|
||||
import { QueryError } from '@/components/ui/query-error';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Card } from '@/components/v2/card';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
|
|
@ -17,7 +22,7 @@ import { subDays } from '@/lib/date-time';
|
|||
import { useFormattedNumber } from '@/lib/hooks';
|
||||
import { cn, pluralize } from '@/lib/utils';
|
||||
import { UTCDate } from '@date-fns/utc';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
|
||||
const TargetCard_TargetFragment = graphql(`
|
||||
fragment TargetCard_TargetFragment on Target {
|
||||
|
|
@ -201,7 +206,17 @@ const TargetCard = (props: {
|
|||
);
|
||||
};
|
||||
|
||||
const ProjectsPageContent = (props: { organizationId: string; projectId: string }) => {
|
||||
export const ProjectIndexRouteSearch = z.object({
|
||||
search: z.string().optional(),
|
||||
sortBy: z.enum(['requests', 'versions', 'name']).optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
});
|
||||
|
||||
type RouteSearchProps = z.infer<typeof ProjectIndexRouteSearch>;
|
||||
|
||||
const ProjectsPageContent = (
|
||||
props: { organizationId: string; projectId: string } & RouteSearchProps,
|
||||
) => {
|
||||
const period = useRef<{
|
||||
from: string;
|
||||
to: string;
|
||||
|
|
@ -216,6 +231,19 @@ const ProjectsPageContent = (props: { organizationId: string; projectId: string
|
|||
period.current = { from, to };
|
||||
}
|
||||
|
||||
// Sort by requests by default
|
||||
const sortKey = props.sortBy ?? 'requests';
|
||||
|
||||
const sortOrder =
|
||||
props.sortOrder === 'asc'
|
||||
? -1
|
||||
: // if the sort order is not set, sort by name in ascending order by default
|
||||
!props.sortOrder && props.sortBy === 'name'
|
||||
? -1
|
||||
: // if the sort order is not set, sort in descending order by default
|
||||
1;
|
||||
const router = useRouter();
|
||||
|
||||
const [query] = useQuery({
|
||||
query: ProjectOverviewPageQuery,
|
||||
variables: {
|
||||
|
|
@ -228,11 +256,43 @@ const ProjectsPageContent = (props: { organizationId: string; projectId: string
|
|||
});
|
||||
|
||||
const targetConnection = query.data?.targets;
|
||||
const targets = targetConnection?.nodes;
|
||||
|
||||
const targets = useMemo(() => {
|
||||
if (!targetConnection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchPhrase = props.search;
|
||||
const newTargets = searchPhrase
|
||||
? targetConnection.nodes.filter(target =>
|
||||
target.name.toLowerCase().includes(searchPhrase.toLowerCase()),
|
||||
)
|
||||
: targetConnection.nodes.slice();
|
||||
|
||||
return newTargets.sort((a, b) => {
|
||||
const diffRequests = b.totalRequests - a.totalRequests;
|
||||
const diffVersions = b.schemaVersionsCount - a.schemaVersionsCount;
|
||||
|
||||
if (sortKey === 'requests' && diffRequests !== 0) {
|
||||
return diffRequests * sortOrder;
|
||||
}
|
||||
|
||||
if (sortKey === 'versions' && diffVersions !== 0) {
|
||||
return diffVersions * sortOrder;
|
||||
}
|
||||
|
||||
if (sortKey === 'name') {
|
||||
return a.name.localeCompare(b.name) * sortOrder * -1;
|
||||
}
|
||||
|
||||
// falls back to sort by name in ascending order
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [targetConnection, props.search, sortKey, sortOrder]);
|
||||
|
||||
const highestNumberOfRequests = useMemo(() => {
|
||||
if (targets?.length) {
|
||||
return targets.reduce((max, target) => {
|
||||
if (targetConnection?.nodes?.length) {
|
||||
return targetConnection.nodes.reduce((max, target) => {
|
||||
return Math.max(
|
||||
max,
|
||||
target.requestsOverTime.reduce((max, { value }) => Math.max(max, value), 0),
|
||||
|
|
@ -241,7 +301,7 @@ const ProjectsPageContent = (props: { organizationId: string; projectId: string
|
|||
}
|
||||
|
||||
return 100;
|
||||
}, [targets]);
|
||||
}, [targetConnection?.nodes]);
|
||||
|
||||
if (query.error) {
|
||||
return <QueryError organizationId={props.organizationId} error={query.error} />;
|
||||
|
|
@ -255,50 +315,124 @@ const ProjectsPageContent = (props: { organizationId: string; projectId: string
|
|||
className="flex justify-between gap-12"
|
||||
>
|
||||
<div className="grow">
|
||||
<div className="py-6">
|
||||
<Title>Targets</Title>
|
||||
<Subtitle>A list of available targets in your project.</Subtitle>
|
||||
<div className="flex flex-row items-center justify-between py-6">
|
||||
<div>
|
||||
<Title>Targets</Title>
|
||||
<Subtitle>A list of available targets in your project.</Subtitle>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<div className="relative">
|
||||
<SearchIcon className="text-muted-foreground absolute left-2.5 top-2.5 size-4" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
value={props.search}
|
||||
onChange={event => {
|
||||
void router.navigate({
|
||||
search(params) {
|
||||
return {
|
||||
...params,
|
||||
search: event.target.value,
|
||||
};
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="bg-background w-full rounded-lg pl-8 md:w-[200px] lg:w-[336px]"
|
||||
/>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-4 h-8" />
|
||||
<Select
|
||||
value={props.sortBy ?? 'requests'}
|
||||
onValueChange={value => {
|
||||
void router.navigate({
|
||||
search(params) {
|
||||
return {
|
||||
...params,
|
||||
sortBy: value,
|
||||
};
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="hover:bg-accent bg-transparent">
|
||||
{props.sortBy === 'versions'
|
||||
? 'Schema Versions'
|
||||
: props.sortBy === 'name'
|
||||
? 'Name'
|
||||
: 'Requests'}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="requests">
|
||||
<div className="font-bold">Requests</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
GraphQL requests made in the last {days} days.
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="versions">
|
||||
<div className="font-bold">Schema Versions</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Schemas published in last {days} days.
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="name">
|
||||
<div className="font-bold">Name</div>
|
||||
<div className="text-muted-foreground text-xs">Sort by target name.</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
className="shrink-0"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
void router.navigate({
|
||||
search(params) {
|
||||
return {
|
||||
...params,
|
||||
sortOrder: props.sortOrder === 'asc' ? 'desc' : 'asc',
|
||||
};
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{props.sortOrder === 'asc' ? (
|
||||
<MoveUpIcon className="size-4" />
|
||||
) : (
|
||||
<MoveDownIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'grow',
|
||||
targets?.length === 0 ? '' : 'grid grid-cols-2 items-stretch gap-5 xl:grid-cols-3',
|
||||
targetConnection?.total === 0
|
||||
? ''
|
||||
: 'grid grid-cols-2 items-stretch gap-5 xl:grid-cols-3',
|
||||
)}
|
||||
>
|
||||
{targets ? (
|
||||
targets.length === 0 ? (
|
||||
{targetConnection ? (
|
||||
targetConnection?.total === 0 ? (
|
||||
<EmptyList
|
||||
title="Hive is waiting for your first target"
|
||||
description='You can create a target by clicking the "New Target" button'
|
||||
docsUrl="/management/targets#create-a-new-target"
|
||||
/>
|
||||
) : (
|
||||
targets
|
||||
.sort((a, b) => {
|
||||
const diffOperations = b.totalRequests - a.totalRequests;
|
||||
if (diffOperations !== 0) {
|
||||
return diffOperations;
|
||||
}
|
||||
|
||||
const diffVersions = b.schemaVersionsCount - a.schemaVersionsCount;
|
||||
if (diffVersions !== 0) {
|
||||
return diffVersions;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map(target => (
|
||||
<TargetCard
|
||||
key={target.id}
|
||||
target={target}
|
||||
days={days}
|
||||
highestNumberOfRequests={highestNumberOfRequests}
|
||||
requestsOverTime={target.requestsOverTime}
|
||||
schemaVersionsCount={target.schemaVersionsCount}
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
))
|
||||
targets.map(target => (
|
||||
<TargetCard
|
||||
key={target.id}
|
||||
target={target}
|
||||
days={days}
|
||||
highestNumberOfRequests={highestNumberOfRequests}
|
||||
requestsOverTime={target.requestsOverTime}
|
||||
schemaVersionsCount={target.schemaVersionsCount}
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -346,11 +480,19 @@ const ProjectOverviewPageQuery = graphql(`
|
|||
}
|
||||
`);
|
||||
|
||||
export function ProjectPage(props: { organizationId: string; projectId: string }): ReactElement {
|
||||
export function ProjectPage(
|
||||
props: { organizationId: string; projectId: string } & RouteSearchProps,
|
||||
): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Targets" />
|
||||
<ProjectsPageContent organizationId={props.organizationId} projectId={props.projectId} />
|
||||
<ProjectsPageContent
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
search={props.search}
|
||||
sortBy={props.sortBy}
|
||||
sortOrder={props.sortOrder}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import { DevPage } from './pages/dev';
|
|||
import { IndexPage } from './pages/index';
|
||||
import { LogoutPage } from './pages/logout';
|
||||
import { ManagePage } from './pages/manage';
|
||||
import { OrganizationPage } from './pages/organization';
|
||||
import { OrganizationIndexRouteSearch, OrganizationPage } from './pages/organization';
|
||||
import { JoinOrganizationPage } from './pages/organization-join';
|
||||
import { OrganizationMembersPage } from './pages/organization-members';
|
||||
import { NewOrgPage } from './pages/organization-new';
|
||||
|
|
@ -48,7 +48,7 @@ import { OrganizationSubscriptionManagePage } from './pages/organization-subscri
|
|||
import { OrganizationSupportPage } from './pages/organization-support';
|
||||
import { OrganizationSupportTicketPage } from './pages/organization-support-ticket';
|
||||
import { OrganizationTransferPage } from './pages/organization-transfer';
|
||||
import { ProjectPage } from './pages/project';
|
||||
import { ProjectIndexRouteSearch, ProjectPage } from './pages/project';
|
||||
import { ProjectAlertsPage } from './pages/project-alerts';
|
||||
import { ProjectPolicyPage } from './pages/project-policy';
|
||||
import { ProjectSettingsPage } from './pages/project-settings';
|
||||
|
|
@ -345,9 +345,18 @@ const organizationRoute = createRoute({
|
|||
const organizationIndexRoute = createRoute({
|
||||
getParentRoute: () => organizationRoute,
|
||||
path: '/',
|
||||
validateSearch: OrganizationIndexRouteSearch.parse,
|
||||
component: function OrganizationRoute() {
|
||||
const { organizationId } = organizationRoute.useParams();
|
||||
return <OrganizationPage organizationId={organizationId} />;
|
||||
const { search, sortBy, sortOrder } = organizationIndexRoute.useSearch();
|
||||
return (
|
||||
<OrganizationPage
|
||||
organizationId={organizationId}
|
||||
search={search}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
/>
|
||||
);
|
||||
},
|
||||
notFoundComponent: NotFound,
|
||||
errorComponent: ErrorComponent,
|
||||
|
|
@ -457,9 +466,19 @@ const projectRoute = createRoute({
|
|||
const projectIndexRoute = createRoute({
|
||||
getParentRoute: () => projectRoute,
|
||||
path: '/',
|
||||
validateSearch: ProjectIndexRouteSearch.parse,
|
||||
component: function ProjectRoute() {
|
||||
const { organizationId, projectId } = projectIndexRoute.useParams();
|
||||
return <ProjectPage organizationId={organizationId} projectId={projectId} />;
|
||||
const { search, sortBy, sortOrder } = projectIndexRoute.useSearch();
|
||||
return (
|
||||
<ProjectPage
|
||||
organizationId={organizationId}
|
||||
projectId={projectId}
|
||||
search={search}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16968,10 +16968,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.596.0
|
||||
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/client-sts': 3.596.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))
|
||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
'@aws-sdk/middleware-logger': 3.577.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.577.0
|
||||
|
|
@ -17076,13 +17076,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/client-sso-oidc@3.596.0':
|
||||
'@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||
'@aws-sdk/client-sts': 3.596.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))
|
||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
'@aws-sdk/middleware-logger': 3.577.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.577.0
|
||||
|
|
@ -17119,6 +17119,7 @@ snapshots:
|
|||
'@smithy/util-utf8': 3.0.0
|
||||
tslib: 2.6.3
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/client-sso-oidc@3.614.0(@aws-sdk/client-sts@3.614.0)':
|
||||
|
|
@ -17252,13 +17253,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)':
|
||||
'@aws-sdk/client-sts@3.596.0':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.596.0
|
||||
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))
|
||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
'@aws-sdk/middleware-logger': 3.577.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.577.0
|
||||
|
|
@ -17295,7 +17296,6 @@ snapshots:
|
|||
'@smithy/util-utf8': 3.0.0
|
||||
tslib: 2.6.3
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/client-sts@3.614.0':
|
||||
|
|
@ -17401,14 +17401,14 @@ snapshots:
|
|||
'@smithy/util-stream': 3.0.6
|
||||
tslib: 2.6.3
|
||||
|
||||
'@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))':
|
||||
'@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))(@aws-sdk/client-sts@3.596.0)':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||
'@aws-sdk/client-sts': 3.596.0
|
||||
'@aws-sdk/credential-provider-env': 3.587.0
|
||||
'@aws-sdk/credential-provider-http': 3.596.0
|
||||
'@aws-sdk/credential-provider-process': 3.587.0
|
||||
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))
|
||||
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))
|
||||
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/credential-provider-imds': 3.1.3
|
||||
'@smithy/property-provider': 3.1.3
|
||||
|
|
@ -17437,14 +17437,14 @@ snapshots:
|
|||
- '@aws-sdk/client-sso-oidc'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))':
|
||||
'@aws-sdk/credential-provider-node@3.596.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))(@aws-sdk/client-sts@3.596.0)':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.587.0
|
||||
'@aws-sdk/credential-provider-http': 3.596.0
|
||||
'@aws-sdk/credential-provider-ini': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))
|
||||
'@aws-sdk/credential-provider-ini': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/credential-provider-process': 3.587.0
|
||||
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))
|
||||
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))
|
||||
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/credential-provider-imds': 3.1.3
|
||||
'@smithy/property-provider': 3.1.3
|
||||
|
|
@ -17491,10 +17491,10 @@ snapshots:
|
|||
'@smithy/types': 3.3.0
|
||||
tslib: 2.6.3
|
||||
|
||||
'@aws-sdk/credential-provider-sso@3.592.0(@aws-sdk/client-sso-oidc@3.596.0)':
|
||||
'@aws-sdk/credential-provider-sso@3.592.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso': 3.592.0
|
||||
'@aws-sdk/token-providers': 3.587.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||
'@aws-sdk/token-providers': 3.587.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/property-provider': 3.1.3
|
||||
'@smithy/shared-ini-file-loader': 3.1.3
|
||||
|
|
@ -17517,9 +17517,9 @@ snapshots:
|
|||
- '@aws-sdk/client-sso-oidc'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0))':
|
||||
'@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||
'@aws-sdk/client-sts': 3.596.0
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/property-provider': 3.1.3
|
||||
'@smithy/types': 3.3.0
|
||||
|
|
@ -17689,9 +17689,9 @@ snapshots:
|
|||
'@smithy/types': 3.3.0
|
||||
tslib: 2.6.3
|
||||
|
||||
'@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)':
|
||||
'@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0))':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso-oidc': 3.596.0
|
||||
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/property-provider': 3.1.3
|
||||
'@smithy/shared-ini-file-loader': 3.1.3
|
||||
|
|
|
|||
Loading…
Reference in a new issue