Replace "Operations" with "Insights" (#2982)

This commit is contained in:
Kamil Kisiela 2023-09-29 12:34:11 +02:00 committed by GitHub
parent be37a9807e
commit 951322cdef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 365 additions and 240 deletions

View file

@ -3,7 +3,7 @@ import NextLink from 'next/link';
import clsx from 'clsx';
import { useMutation, useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { SchemaEditor } from '@/components/schema-editor';
import { ChangesBlock, labelize } from '@/components/target/history/errors-and-changes';
import { Subtitle, Title } from '@/components/ui/page';
@ -766,7 +766,7 @@ function ChecksPageContent() {
return (
<>
<TargetLayout
value="checks"
page={Page.Checks}
className="h-full"
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}

View file

@ -1,7 +1,7 @@
import { ReactElement } from 'react';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { SchemaExplorerFilter } from '@/components/target/explorer/filter';
import { GraphQLObjectTypeComponent } from '@/components/target/explorer/object-type';
import {
@ -163,7 +163,7 @@ function ExplorerPageContent() {
return (
<TargetLayout
value="explorer"
page={Page.Explorer}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}

View file

@ -1,6 +1,6 @@
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { GraphQLEnumTypeComponent } from '@/components/target/explorer/enum-type';
import { SchemaExplorerFilter } from '@/components/target/explorer/filter';
import { GraphQLInputObjectTypeComponent } from '@/components/target/explorer/input-object-type';
@ -194,7 +194,7 @@ function TypeExplorerPageContent({ typename }: { typename: string }) {
return (
<TargetLayout
value="explorer"
page={Page.Explorer}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}

View file

@ -2,7 +2,7 @@ import { ReactElement, useCallback, useState } from 'react';
import NextLink from 'next/link';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { VersionErrorsAndChanges } from '@/components/target/history/errors-and-changes';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
@ -392,7 +392,7 @@ function HistoryPageContent() {
return (
<TargetLayout
value="history"
page={Page.History}
className="h-full"
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}

View file

@ -2,7 +2,7 @@ import { ChangeEventHandler, ReactElement, useCallback, useEffect, useRef, useSt
import { useQuery } from 'urql';
import { useDebouncedCallback } from 'use-debounce';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { MarkAsValid } from '@/components/target/history/MarkAsValid';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
@ -281,7 +281,7 @@ const TargetSchemaPageQuery = graphql(`
}
`);
function Page() {
function TargetSchemaPage() {
const router = useRouteSelector();
const [query] = useQuery({
query: TargetSchemaPageQuery,
@ -306,7 +306,7 @@ function Page() {
return (
<TargetLayout
value="schema"
page={Page.Schema}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}
@ -335,7 +335,7 @@ function SchemaPage(): ReactElement {
return (
<>
<MetaTitle title="Schema" />
<Page />
<TargetSchemaPage />
</>
);
}

View file

@ -1,13 +1,13 @@
import { ReactElement, useState } from 'react';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import {
ClientsFilterTrigger,
OperationsFilterTrigger,
} from '@/components/target/operations/Filters';
import { OperationsList } from '@/components/target/operations/List';
import { OperationsStats } from '@/components/target/operations/Stats';
} from '@/components/target/insights/Filters';
import { OperationsList } from '@/components/target/insights/List';
import { OperationsStats } from '@/components/target/insights/Stats';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
import { EmptyList, MetaTitle, RadixSelect } from '@/components/v2';
@ -44,7 +44,7 @@ function OperationsView({
<>
<div className="py-6 flex flex-row items-center justify-between">
<div>
<Title>Operations</Title>
<Title>Insights</Title>
<Subtitle>Observe GraphQL requests and see how the API is consumed.</Subtitle>
</div>
<div className="flex justify-end gap-x-2">
@ -76,6 +76,7 @@ function OperationsView({
operationsFilter={selectedOperations}
clientNamesFilter={selectedClients}
resolution={resolution}
dateRangeText={displayDateRangeLabel(dateRangeKey)}
mode="operation-list"
/>
<OperationsList
@ -148,7 +149,7 @@ function TargetOperationsPageContent() {
return (
<TargetLayout
value="operations"
page={Page.Insights}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}
@ -177,10 +178,10 @@ function TargetOperationsPageContent() {
);
}
function OperationsPage(): ReactElement {
function InsightsPage(): ReactElement {
return (
<>
<MetaTitle title="Operations" />
<MetaTitle title="Insights" />
<TargetOperationsPageContent />
</>
);
@ -188,4 +189,4 @@ function OperationsPage(): ReactElement {
export const getServerSideProps = withSessionProtection();
export default authenticated(OperationsPage);
export default authenticated(InsightsPage);

View file

@ -4,9 +4,9 @@ import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { Section } from '@/components/common';
import { GraphQLHighlight } from '@/components/common/GraphQLSDLBlock';
import { TargetLayout } from '@/components/layouts/target';
import { ClientsFilterTrigger } from '@/components/target/operations/Filters';
import { OperationsStats } from '@/components/target/operations/Stats';
import { Page, TargetLayout } from '@/components/layouts/target';
import { ClientsFilterTrigger } from '@/components/target/insights/Filters';
import { OperationsStats } from '@/components/target/insights/Stats';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
import { EmptyList, MetaTitle, RadixSelect } from '@/components/v2';
@ -87,7 +87,7 @@ function OperationView({
<div className="py-6 flex flex-row items-center justify-between">
<div>
<Title>{operationName}</Title>
<Subtitle>Performance of individual GraphQL operation</Subtitle>
<Subtitle>Insights of individual GraphQL operation</Subtitle>
</div>
<div className="flex justify-end gap-x-2">
<ClientsFilterTrigger
@ -110,6 +110,7 @@ function OperationView({
project={projectCleanId}
target={targetCleanId}
period={dateRange}
dateRangeText={displayDateRangeLabel(dateRangeKey)}
operationsFilter={operationsList}
clientNamesFilter={selectedClients}
resolution={resolution}
@ -128,8 +129,8 @@ function OperationView({
);
}
const TargetOperationPageQuery = graphql(`
query TargetOperationPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) {
const OperationInsightsPageQuery = graphql(`
query OperationInsightsPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) {
organizations {
...TargetLayout_OrganizationConnectionFragment
}
@ -159,7 +160,7 @@ const TargetOperationPageQuery = graphql(`
}
`);
function TargetOperationPageContent({
function OperationInsightsContent({
operationHash,
operationName,
}: {
@ -168,7 +169,7 @@ function TargetOperationPageContent({
}) {
const router = useRouteSelector();
const [query] = useQuery({
query: TargetOperationPageQuery,
query: OperationInsightsPageQuery,
variables: {
organizationId: router.organizationId,
projectId: router.projectId,
@ -190,7 +191,7 @@ function TargetOperationPageContent({
return (
<TargetLayout
value="operations"
page={Page.Insights}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}
@ -221,7 +222,7 @@ function TargetOperationPageContent({
);
}
function OperationPage(): ReactElement {
function OperationInsightsPage(): ReactElement {
const router = useRouter();
const { operationHash, operationName } = router.query;
@ -236,11 +237,11 @@ function OperationPage(): ReactElement {
return (
<>
<MetaTitle title={`Operation ${operationName}`} />
<TargetOperationPageContent operationHash={operationHash} operationName={operationName} />
<OperationInsightsContent operationHash={operationHash} operationName={operationName} />
</>
);
}
export const getServerSideProps = withSessionProtection();
export default authenticated(OperationPage);
export default authenticated(OperationInsightsPage);

View file

@ -7,7 +7,7 @@ import { ActivityIcon, BookIcon, GlobeIcon, HistoryIcon } from 'lucide-react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
@ -289,7 +289,7 @@ function ClientView(props: {
className="text-orange-500 hover:underline hover:underline-offset-2 hover:text-orange-500"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/operations/[operationName]/[operationHash]',
'/[organizationId]/[projectId]/[targetId]/insights/[operationName]/[operationHash]',
query: {
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
@ -350,8 +350,8 @@ function ClientView(props: {
);
}
const ClientOperationsPageQuery = graphql(`
query ClientOperationsPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) {
const ClientInsightsPageQuery = graphql(`
query ClientInsightsPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) {
organizations {
...TargetLayout_OrganizationConnectionFragment
}
@ -381,10 +381,10 @@ const ClientOperationsPageQuery = graphql(`
}
`);
function ClientOperationsPageContent({ clientName }: { clientName: string }) {
function ClientInsightsPageContent({ clientName }: { clientName: string }) {
const router = useRouteSelector();
const [query] = useQuery({
query: ClientOperationsPageQuery,
query: ClientInsightsPageQuery,
variables: {
organizationId: router.organizationId,
projectId: router.projectId,
@ -406,7 +406,7 @@ function ClientOperationsPageContent({ clientName }: { clientName: string }) {
return (
<TargetLayout
value="operations"
page={Page.Insights}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}
@ -436,7 +436,7 @@ function ClientOperationsPageContent({ clientName }: { clientName: string }) {
);
}
function ClientOperationsPage(): ReactElement {
function ClientInsightsPage(): ReactElement {
const router = useRouter();
const { name } = router.query;
@ -447,11 +447,11 @@ function ClientOperationsPage(): ReactElement {
return (
<>
<MetaTitle title={`${name} - client`} />
<ClientOperationsPageContent clientName={name} />
<ClientInsightsPageContent clientName={name} />
</>
);
}
export const getServerSideProps = withSessionProtection();
export default authenticated(ClientOperationsPage);
export default authenticated(ClientInsightsPage);

View file

@ -7,7 +7,7 @@ import { ActivityIcon, BookIcon, GlobeIcon, TabletSmartphoneIcon } from 'lucide-
import AutoSizer from 'react-virtualized-auto-sizer';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
@ -111,7 +111,7 @@ function SchemaCoordinateView(props: {
<div className="py-6 flex flex-row items-center justify-between">
<div>
<Title>{props.coordinate}</Title>
<Subtitle>Detailed view of schema coordinate usage</Subtitle>
<Subtitle>Schema coordinate insights</Subtitle>
</div>
<div className="flex justify-end gap-x-2">
<Select
@ -293,7 +293,7 @@ function SchemaCoordinateView(props: {
className="text-orange-500 hover:underline hover:underline-offset-2 hover:text-orange-500"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/operations/[operationName]/[operationHash]',
'/[organizationId]/[projectId]/[targetId]/insights/[operationName]/[operationHash]',
query: {
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
@ -338,7 +338,7 @@ function SchemaCoordinateView(props: {
className="text-orange-500 hover:underline hover:underline-offset-2 hover:text-orange-500"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/operations/client/[name]',
'/[organizationId]/[projectId]/[targetId]/insights/client/[name]',
query: {
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
@ -423,7 +423,7 @@ function TargetSchemaCoordinatePageContent({ coordinate }: { coordinate: string
return (
<TargetLayout
value="operations"
page={Page.Insights}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}

View file

@ -4,7 +4,7 @@ import { GraphiQL } from 'graphiql';
import { buildSchema } from 'graphql';
import { useMutation, useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { ConnectLabModal } from '@/components/target/laboratory/connect-lab-modal';
import { CreateCollectionModal } from '@/components/target/laboratory/create-collection-modal';
import { CreateOperationModal } from '@/components/target/laboratory/create-operation-modal';
@ -751,7 +751,7 @@ function LaboratoryPageContent() {
return (
<TargetLayout
value="laboratory"
page={Page.Laboratory}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}

View file

@ -6,7 +6,7 @@ import { useFormik } from 'formik';
import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts/target';
import { Page, TargetLayout } from '@/components/layouts/target';
import { SchemaEditor } from '@/components/schema-editor';
import { CDNAccessTokens } from '@/components/target/settings/cdn-access-tokens';
import { Subtitle, Title } from '@/components/ui/page';
@ -963,7 +963,7 @@ function TargetSettingsContent() {
return (
<TargetLayout
value="settings"
page={Page.Settings}
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
me={me ?? null}

View file

@ -7,12 +7,12 @@ import { Globe, History } from 'lucide-react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { ProjectLayout } from '@/components/layouts/project';
import { Page, ProjectLayout } from '@/components/layouts/project';
import {
createEmptySeries,
fullSeries,
resolutionToMilliseconds,
} from '@/components/target/operations/utils';
} from '@/components/target/insights/utils';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
@ -208,7 +208,7 @@ const TargetCard = (props: {
);
};
const Page = () => {
const ProjectsPageContent = () => {
const router = useRouteSelector();
const period = useRef<{
from: string;
@ -260,7 +260,7 @@ const Page = () => {
return (
<ProjectLayout
value="targets"
page={Page.Targets}
className="flex justify-between gap-12"
currentOrganization={currentOrganization ?? null}
currentProject={currentProject ?? null}
@ -371,7 +371,7 @@ function ProjectsPage(): ReactElement {
return (
<>
<MetaTitle title="Targets" />
<Page />
<ProjectsPageContent />
</>
);
}

View file

@ -1,7 +1,7 @@
import { ReactElement, useState } from 'react';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { ProjectLayout } from '@/components/layouts/project';
import { Page, ProjectLayout } from '@/components/layouts/project';
import { AlertsTable, AlertsTable_AlertFragment } from '@/components/project/alerts/alerts-table';
import {
ChannelsTable,
@ -234,7 +234,7 @@ function AlertsPageContent() {
currentProject={currentProject ?? null}
organizations={organizationConnection ?? null}
me={me ?? null}
value="alerts"
page={Page.Alerts}
className="flex flex-col gap-y-10"
>
<div>

View file

@ -1,7 +1,7 @@
import { ReactElement } from 'react';
import { useMutation, useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { ProjectLayout } from '@/components/layouts/project';
import { Page, ProjectLayout } from '@/components/layouts/project';
import { PolicySettings } from '@/components/policy/policy-settings';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Subtitle, Title } from '@/components/ui/page';
@ -113,7 +113,7 @@ function ProjectPolicyContent() {
currentProject={currentProject ?? null}
organizations={organizationConnection ?? null}
me={me ?? null}
value="policy"
page={Page.Policy}
className="flex flex-col gap-y-10"
>
<div>

View file

@ -3,7 +3,7 @@ import { useFormik } from 'formik';
import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { authenticated } from '@/components/authenticated-container';
import { ProjectLayout } from '@/components/layouts/project';
import { Page, ProjectLayout } from '@/components/layouts/project';
import { ExternalCompositionSettings } from '@/components/project/settings/external-composition';
import { ModelMigrationSettings } from '@/components/project/settings/model-migration';
import { Button } from '@/components/ui/button';
@ -309,7 +309,7 @@ function ProjectSettingsContent() {
currentProject={currentProject ?? null}
organizations={organizationConnection ?? null}
me={me ?? null}
value="settings"
page={Page.Settings}
className="flex flex-col gap-y-10"
>
<div>

View file

@ -7,12 +7,12 @@ import { Globe, History } from 'lucide-react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import {
createEmptySeries,
fullSeries,
resolutionToMilliseconds,
} from '@/components/target/operations/utils';
} from '@/components/target/insights/utils';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
@ -319,7 +319,7 @@ function OrganizationPageContent() {
return (
<OrganizationLayout
value="overview"
page={Page.Overview}
className="flex justify-between gap-12"
currentOrganization={currentOrganization ?? null}
organizations={organizationConnection ?? null}

View file

@ -3,7 +3,7 @@ import NextLink from 'next/link';
import { useMutation, useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { Section } from '@/components/common';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import { BillingPaymentMethod } from '@/components/organization/billing/BillingPaymentMethod';
import { BillingPlanPicker } from '@/components/organization/billing/BillingPlanPicker';
import { PlanSummary } from '@/components/organization/billing/PlanSummary';
@ -471,7 +471,7 @@ function ManageSubscriptionPageContent() {
return (
<OrganizationLayout
value="subscription"
page={Page.Subscription}
className="flex flex-col gap-y-10"
currentOrganization={currentOrganization}
organizations={organizationConnection}

View file

@ -3,7 +3,7 @@ import { useFormik } from 'formik';
import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { authenticated } from '@/components/authenticated-container';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Subtitle, Title } from '@/components/ui/page';
@ -270,7 +270,7 @@ const OrganizationInvitations = (props: {
) : null;
};
function Page(props: {
function PageContent(props: {
organization: FragmentType<typeof Page_OrganizationFragment>;
me?: FragmentType<typeof OrganizationMembersPage_MeFragment>;
}) {
@ -441,13 +441,13 @@ function SettingsPageContent() {
return (
<OrganizationLayout
value="members"
page={Page.Members}
className="flex flex-col gap-y-10"
currentOrganization={currentOrganization ?? null}
organizations={organizationConnection ?? null}
me={me ?? null}
>
{currentOrganization ? <Page organization={currentOrganization} me={me} /> : null}
{currentOrganization ? <PageContent organization={currentOrganization} me={me} /> : null}
</OrganizationLayout>
);
}

View file

@ -1,7 +1,7 @@
import { ReactElement } from 'react';
import { useMutation, useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import { PolicySettings } from '@/components/policy/policy-settings';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
@ -98,7 +98,7 @@ function PolicyPageContent() {
return (
<OrganizationLayout
value="policy"
page={Page.Policy}
className="flex flex-col gap-y-10"
currentOrganization={currentOrganization ?? null}
organizations={organizationConnection ?? null}

View file

@ -4,7 +4,7 @@ import { useFormik } from 'formik';
import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { authenticated } from '@/components/authenticated-container';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import { OIDCIntegrationSection } from '@/components/organization/settings/oidc-integration-section';
import { Button } from '@/components/ui/button';
import {
@ -450,7 +450,7 @@ function SettingsPageContent() {
return (
<OrganizationLayout
value="settings"
page={Page.Settings}
className="flex flex-col gap-y-10"
currentOrganization={currentOrganization ?? null}
organizations={organizationConnection ?? null}

View file

@ -3,7 +3,7 @@ import NextLink from 'next/link';
import { endOfMonth, startOfMonth } from 'date-fns';
import { useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import { BillingView } from '@/components/organization/billing/Billing';
import { CurrencyFormatter } from '@/components/organization/billing/helpers';
import { InvoicesList } from '@/components/organization/billing/InvoicesList';
@ -116,7 +116,7 @@ function SubscriptionPageContent() {
return (
<OrganizationLayout
value="subscription"
page={Page.Subscription}
className="flex flex-col gap-y-10"
currentOrganization={currentOrganization}
organizations={organizationConnection}

View file

@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
import { useMutation, useQuery } from 'urql';
import { z } from 'zod';
import { authenticated } from '@/components/authenticated-container';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import { Priority, priorityDescription, Status } from '@/components/organization/support';
import { Button } from '@/components/ui/button';
import {
@ -435,7 +435,7 @@ function SupportPageContent() {
return (
<OrganizationLayout
value="support"
page={Page.Support}
className="flex flex-col gap-y-10"
currentOrganization={currentOrganization ?? null}
organizations={organizationConnection ?? null}

View file

@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
import { useMutation, useQuery } from 'urql';
import { z } from 'zod';
import { authenticated } from '@/components/authenticated-container';
import { OrganizationLayout } from '@/components/layouts/organization';
import { OrganizationLayout, Page } from '@/components/layouts/organization';
import { priorityDescription, statusDescription } from '@/components/organization/support';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
@ -301,7 +301,7 @@ const SupportTicketPageQuery = graphql(`
}
`);
function SupportTicketPage() {
function SupportTicketPageContent() {
const router = useRouteSelector();
const ticketId = router.query.ticketId as string;
const [query, refetchQuery] = useQuery({
@ -330,7 +330,7 @@ function SupportTicketPage() {
return (
<OrganizationLayout
value="support"
page={Page.Support}
className="flex flex-col gap-y-10"
currentOrganization={currentOrganization ?? null}
organizations={organizationConnection ?? null}
@ -352,18 +352,18 @@ function SupportTicketPage() {
);
}
function Page() {
function SupportTicketPage() {
const router = useRouteSelector();
const ticketId = router.query.ticketId as string;
return (
<>
<MetaTitle title={`Support Ticket #${ticketId}`} />
<SupportTicketPage />
<SupportTicketPageContent />
</>
);
}
export const getServerSideProps = withSessionProtection();
export default authenticated(Page);
export default authenticated(SupportTicketPage);

View file

@ -18,7 +18,7 @@ import { useRouteSelector, useToggle } from '@/lib/hooks';
import { ProPlanBilling } from '../organization/billing/ProPlanBillingWarm';
import { RateLimitWarn } from '../organization/billing/RateLimitWarn';
enum TabValue {
export enum Page {
Overview = 'overview',
Members = 'members',
Settings = 'settings',
@ -61,11 +61,11 @@ const OrganizationLayout_OrganizationConnectionFragment = graphql(`
export function OrganizationLayout({
children,
value,
page,
className,
...props
}: {
value?: 'overview' | 'members' | 'settings' | 'subscription' | 'policy' | 'support';
page?: Page;
className?: string;
me: FragmentType<typeof OrganizationLayout_MeFragment> | null;
currentOrganization: FragmentType<typeof OrganizationLayout_CurrentOrganizationFragment> | null;
@ -138,9 +138,9 @@ export function OrganizationLayout({
<div className="relative border-b border-gray-800">
<div className="container flex justify-between items-center">
{currentOrganization && meInCurrentOrg ? (
<Tabs value={value}>
<Tabs value={page}>
<Tabs.List>
<Tabs.Trigger value={TabValue.Overview} asChild>
<Tabs.Trigger value={Page.Overview} asChild>
<NextLink
href={{
pathname: '/[organizationId]',
@ -151,13 +151,13 @@ export function OrganizationLayout({
</NextLink>
</Tabs.Trigger>
{canAccessOrganization(OrganizationAccessScope.Members, meInCurrentOrg) && (
<Tabs.Trigger value={TabValue.Members} asChild>
<Tabs.Trigger value={Page.Members} asChild>
<NextLink
href={{
pathname: '/[organizationId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
tab: TabValue.Members,
tab: Page.Members,
},
}}
>
@ -167,26 +167,26 @@ export function OrganizationLayout({
)}
{canAccessOrganization(OrganizationAccessScope.Settings, meInCurrentOrg) && (
<>
<Tabs.Trigger value={TabValue.Policy} asChild>
<Tabs.Trigger value={Page.Policy} asChild>
<NextLink
href={{
pathname: '/[organizationId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
tab: TabValue.Policy,
tab: Page.Policy,
},
}}
>
Policy
</NextLink>
</Tabs.Trigger>
<Tabs.Trigger value={TabValue.Settings} asChild>
<Tabs.Trigger value={Page.Settings} asChild>
<NextLink
href={{
pathname: '/[organizationId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
tab: TabValue.Settings,
tab: Page.Settings,
},
}}
>
@ -197,13 +197,13 @@ export function OrganizationLayout({
)}
{canAccessOrganization(OrganizationAccessScope.Read, meInCurrentOrg) &&
env.zendeskSupport && (
<Tabs.Trigger value={TabValue.Support} asChild>
<Tabs.Trigger value={Page.Support} asChild>
<NextLink
href={{
pathname: '/[organizationId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
tab: TabValue.Support,
tab: Page.Support,
},
}}
>
@ -213,13 +213,13 @@ export function OrganizationLayout({
)}
{getIsStripeEnabled() &&
canAccessOrganization(OrganizationAccessScope.Settings, meInCurrentOrg) && (
<Tabs.Trigger value={TabValue.Subscription} asChild>
<Tabs.Trigger value={Page.Subscription} asChild>
<NextLink
href={{
pathname: '/[organizationId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
tab: TabValue.Subscription,
tab: Page.Subscription,
},
}}
>

View file

@ -11,7 +11,7 @@ import { canAccessProject, ProjectAccessScope, useProjectAccess } from '@/lib/ac
import { useRouteSelector, useToggle } from '@/lib/hooks';
import { ProjectMigrationToast } from '../project/migration-toast';
enum TabValue {
export enum Page {
Targets = 'targets',
Alerts = 'alerts',
Policy = 'policy',
@ -72,11 +72,11 @@ const ProjectLayout_ProjectConnectionFragment = graphql(`
export function ProjectLayout({
children,
value,
page,
className,
...props
}: {
value: 'targets' | 'alerts' | 'settings' | 'policy';
page: Page;
className?: string;
me: FragmentType<typeof ProjectLayout_MeFragment> | null;
currentOrganization: FragmentType<typeof ProjectLayout_CurrentOrganizationFragment> | null;
@ -169,16 +169,16 @@ export function ProjectLayout({
</div>
</header>
{value === 'settings' || currentProject?.registryModel !== 'LEGACY' ? null : (
{page === Page.Settings || currentProject?.registryModel !== 'LEGACY' ? null : (
<ProjectMigrationToast orgId={orgId} projectId={currentProject.cleanId} />
)}
<div className="relative border-b border-gray-800">
<div className="container flex justify-between items-center">
{currentOrganization && currentProject ? (
<Tabs value={value}>
<Tabs value={page}>
<Tabs.List>
<Tabs.Trigger value={TabValue.Targets} asChild>
<Tabs.Trigger value={Page.Targets} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]',
@ -192,14 +192,14 @@ export function ProjectLayout({
</NextLink>
</Tabs.Trigger>
{canAccessProject(ProjectAccessScope.Alerts, currentOrganization.me) && (
<Tabs.Trigger value={TabValue.Alerts} asChild>
<Tabs.Trigger value={Page.Alerts} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
tab: TabValue.Alerts,
tab: Page.Alerts,
},
}}
>
@ -209,28 +209,28 @@ export function ProjectLayout({
)}
{canAccessProject(ProjectAccessScope.Settings, currentOrganization.me) && (
<>
<Tabs.Trigger value={TabValue.Policy} asChild>
<Tabs.Trigger value={Page.Policy} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
tab: TabValue.Policy,
tab: Page.Policy,
},
}}
>
Policy
</NextLink>
</Tabs.Trigger>
<Tabs.Trigger value={TabValue.Settings} asChild>
<Tabs.Trigger value={Page.Settings} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/view/[tab]',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
tab: TabValue.Settings,
tab: Page.Settings,
},
}}
>

View file

@ -12,12 +12,12 @@ import { useRouteSelector, useToggle } from '@/lib/hooks';
import { cn } from '@/lib/utils';
import { ProjectMigrationToast } from '../project/migration-toast';
enum TabValue {
export enum Page {
Schema = 'schema',
Explorer = 'explorer',
Checks = 'checks',
History = 'history',
Operations = 'operations',
Insights = 'insights',
Laboratory = 'laboratory',
Settings = 'settings',
}
@ -86,11 +86,11 @@ const TargetLayout_IsCDNEnabledFragment = graphql(`
export const TargetLayout = ({
children,
connect,
value,
page,
className,
...props
}: {
value: 'schema' | 'explorer' | 'checks' | 'history' | 'operations' | 'laboratory' | 'settings';
page: Page;
className?: string;
children: ReactNode;
connect?: ReactNode;
@ -223,11 +223,11 @@ export const TargetLayout = ({
<div className="relative border-b border-gray-800">
<div className="container flex justify-between items-center">
{currentOrganization && currentProject && currentTarget ? (
<Tabs className="flex h-full grow flex-col" value={value}>
<Tabs className="flex h-full grow flex-col" value={page}>
<Tabs.List>
{canAccessSchema && (
<>
<Tabs.Trigger value={TabValue.Schema} asChild>
<Tabs.Trigger value={Page.Schema} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]',
@ -241,7 +241,7 @@ export const TargetLayout = ({
Schema
</NextLink>
</Tabs.Trigger>
<Tabs.Trigger value={TabValue.Checks} asChild>
<Tabs.Trigger value={Page.Checks} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/checks',
@ -255,7 +255,7 @@ export const TargetLayout = ({
Checks
</NextLink>
</Tabs.Trigger>
<Tabs.Trigger value={TabValue.Explorer} asChild>
<Tabs.Trigger value={Page.Explorer} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/explorer',
@ -269,7 +269,7 @@ export const TargetLayout = ({
Explorer
</NextLink>
</Tabs.Trigger>
<Tabs.Trigger value={TabValue.History} asChild>
<Tabs.Trigger value={Page.History} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/history',
@ -283,10 +283,10 @@ export const TargetLayout = ({
History
</NextLink>
</Tabs.Trigger>
<Tabs.Trigger value={TabValue.Operations} asChild>
<Tabs.Trigger value={Page.Insights} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/operations',
pathname: '/[organizationId]/[projectId]/[targetId]/insights',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
@ -294,10 +294,10 @@ export const TargetLayout = ({
},
}}
>
Operations
Insights
</NextLink>
</Tabs.Trigger>
<Tabs.Trigger value={TabValue.Laboratory} asChild>
<Tabs.Trigger value={Page.Laboratory} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/laboratory',
@ -314,7 +314,7 @@ export const TargetLayout = ({
</>
)}
{canAccessSettings && (
<Tabs.Trigger value={TabValue.Settings} asChild>
<Tabs.Trigger value={Page.Settings} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/settings',

View file

@ -121,7 +121,7 @@ export function SchemaExplorerUsageStats(props: {
className="text-orange-500 hover:underline hover:underline-offset-2 hover:text-orange-500"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/operations/[operationName]/[operationHash]',
'/[organizationId]/[projectId]/[targetId]/insights/[operationName]/[operationHash]',
query: {
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
@ -171,7 +171,7 @@ export function SchemaExplorerUsageStats(props: {
className="text-orange-500 hover:underline hover:underline-offset-2 hover:text-orange-500"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/operations/client/[name]',
'/[organizationId]/[projectId]/[targetId]/insights/client/[name]',
query: {
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
@ -527,7 +527,7 @@ export function LinkToCoordinatePage(props: {
className="text-orange-500"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/operations/schema-coordinate/[coordinate]',
'/[organizationId]/[projectId]/[targetId]/insights/schema-coordinate/[coordinate]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,

View file

@ -4,7 +4,19 @@ import clsx from 'clsx';
import { useQuery } from 'urql';
import { useDebouncedCallback } from 'use-debounce';
import { Scale, Section } from '@/components/common';
import { Button, Input, Sortable, Table, TBody, Td, Th, THead, Tooltip, Tr } from '@/components/v2';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Button as OldButton,
Sortable,
Table,
TBody,
Td,
Th,
THead,
Tooltip,
Tr,
} from '@/components/v2';
import { env } from '@/env/frontend';
import { FragmentType, graphql, useFragment } from '@/gql';
import { DateRangeInput } from '@/graphql';
@ -58,12 +70,12 @@ function OperationRow({
return (
<>
<Tr>
<Td className="font-medium truncate">
<Td className="font-medium">
<div className="flex gap-2 items-center">
<Link
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/operations/[operationName]/[operationHash]',
'/[organizationId]/[projectId]/[targetId]/insights/[operationName]/[operationHash]',
query: {
organizationId: organization,
projectId: project,
@ -75,9 +87,9 @@ function OperationRow({
}}
passHref
>
<Button variant="link" as="a">
<OldButton variant="link" as="a" className="block truncate max-w-[300px]">
{operation.name}
</Button>
</OldButton>
</Link>
{operation.name === 'anonymous' && (
<Tooltip.Provider delayDuration={200}>
@ -88,16 +100,16 @@ function OperationRow({
)}
</div>
</Td>
<Td align="center">{operation.kind}</Td>
<Td align="center" className="text-xs">
{operation.kind}
</Td>
<Td align="center">{p90}</Td>
<Td align="center">{p95}</Td>
<Td align="center">{p99}</Td>
<Td align="center">{failureRate}%</Td>
<Td align="center">{count}</Td>
<Td align="right" width="1">
{percentage}%
</Td>
<Td width="1">
<Td align="right">{percentage}%</Td>
<Td>
<Scale value={operation.percentage} size={10} max={100} className="justify-end" />
</Td>
</Tr>
@ -105,34 +117,62 @@ function OperationRow({
);
}
const table = createTable().setRowType<Operation>();
const table = createTable()
.setTableMetaType<{
align: 'left' | 'center' | 'right';
}>()
.setRowType<Operation>();
const columns = [
table.createDataColumn('name', {
header: 'Operations',
enableSorting: false,
meta: {
align: 'left',
},
}),
table.createDataColumn('kind', {
header: 'Kind',
enableSorting: false,
meta: {
align: 'center',
},
}),
table.createDataColumn('p90', {
header: 'p90',
meta: {
align: 'center',
},
}),
table.createDataColumn('p95', {
header: 'p95',
meta: {
align: 'center',
},
}),
table.createDataColumn('p99', {
header: 'p99',
meta: {
align: 'center',
},
}),
table.createDataColumn('failureRate', {
header: 'Failure Rate',
meta: {
align: 'center',
},
}),
table.createDataColumn('requests', {
header: 'Requests',
meta: {
align: 'center',
},
}),
table.createDataColumn('percentage', {
header: 'Traffic',
meta: {
align: 'right',
},
}),
];
@ -191,6 +231,7 @@ function OperationsTable({
}, 500);
const { headers } = tableInstance.getHeaderGroups()[0];
return (
<div className={clsx('rounded-md p-5 border border-gray-800 bg-gray-900/50', className)}>
<Section.Title>Operations</Section.Title>
@ -201,9 +242,11 @@ function OperationsTable({
<Tooltip.Provider>
{headers.map(header => {
const canSort = header.column.getCanSort();
const align: 'center' | 'left' | 'right' =
(header.column.columnDef.meta as any)?.align || 'left';
const name = header.renderHeader();
return (
<Th key={header.id}>
<Th key={header.id} className="text-sm font-semibold" align={align}>
{canSort ? (
<Sortable
sortOrder={header.column.getIsSorted()}
@ -238,11 +281,16 @@ function OperationsTable({
</TBody>
</Table>
<div className="flex items-center gap-2 mt-6">
<Button onClick={firstPage} disabled={!tableInstance.getCanPreviousPage()}>
<Button
onClick={firstPage}
variant="outline"
disabled={!tableInstance.getCanPreviousPage()}
>
First
</Button>
<Button
aria-label="Go to previous page"
variant="outline"
onClick={tableInstance.previousPage}
disabled={!tableInstance.getCanPreviousPage()}
>
@ -253,22 +301,19 @@ function OperationsTable({
</span>
<Button
aria-label="Go to next page"
variant="outline"
onClick={tableInstance.nextPage}
disabled={!tableInstance.getCanNextPage()}
>
<ChevronUpIcon className="rotate-90 h-5 w-auto" />
</Button>
<Button onClick={lastPage} disabled={!tableInstance.getCanNextPage()}>
<Button variant="outline" onClick={lastPage} disabled={!tableInstance.getCanNextPage()}>
Last
</Button>
<div className="ml-6">Go to:</div>
<Input
prefix={
<label htmlFor="page" className="shrink-0">
Go to:
</label>
}
id="page"
size="medium"
className="w-16"
type="number"
defaultValue={tableInstance.getState().pagination.pageIndex + 1}
onChange={e => {

View file

@ -1,11 +1,21 @@
import { ReactElement, useCallback, useMemo, useState } from 'react';
import { differenceInMilliseconds } from 'date-fns';
import ReactECharts from 'echarts-for-react';
import { ChevronUp } from 'lucide-react';
import {
ActivityIcon,
BookIcon,
ChevronUp,
FrownIcon,
GaugeIcon,
GlobeIcon,
PercentIcon,
SmileIcon,
} from 'lucide-react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useQuery } from 'urql';
import { Section } from '@/components/common';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CHART_PRIMARY_COLOR } from '@/constants';
import { FragmentType, graphql, useFragment } from '@/gql';
import { DateRangeInput } from '@/graphql';
@ -19,7 +29,6 @@ import {
useFormattedThroughput,
useRouteSelector,
} from '@/lib/hooks';
import { cn } from '@/lib/utils';
import { useChartStyles } from '@/utils';
import { OperationsFallback } from './Fallback';
import { createEmptySeries, resolutionToMilliseconds } from './utils';
@ -53,41 +62,50 @@ const Stats_GeneralOperationsStatsQuery = graphql(`
}
`);
const classes = {
root: cn('text-center'),
value: cn('font-normal text-3xl text-gray-900 dark:text-white'),
title: cn('text-sm leading-relaxed'),
};
function RequestsStats({ requests = 0 }: { requests?: number }): ReactElement {
function RequestsStats({
requests = 0,
}: {
requests?: number;
dateRangeText: string;
}): ReactElement {
const value = useFormattedNumber(requests);
return (
<div className={classes.root}>
<h2 className={classes.value}>{value}</h2>
<p className={classes.title}>Requests</p>
</div>
<Card className="bg-gray-900/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Requests</CardTitle>
<GlobeIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground">Total requests served</p>
</CardContent>
</Card>
);
}
function UniqueOperationsStats({ operations = 0 }: { operations?: number }): ReactElement {
function UniqueOperationsStats({
operations = 0,
dateRangeText,
}: {
operations?: number;
dateRangeText: string;
}): ReactElement {
const value = useFormattedNumber(operations);
return (
<TooltipProvider>
<div className={classes.root}>
<Tooltip>
<TooltipTrigger>
<h2 className={classes.value}>{value}</h2>
<p className={classes.title}>Unique Operations</p>
</TooltipTrigger>
<TooltipContent>
Count of unique operations that have been requested, taking into account applied
filters.
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
<Card className="bg-gray-900/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Operations</CardTitle>
<BookIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground">
Distinct GraphQL operations in {dateRangeText}
</p>
</CardContent>
</Card>
);
}
@ -97,45 +115,60 @@ function OperationRelativeFrequency({
}: {
allOperationRequests: number;
operationRequests: number;
dateRangeText: string;
}): ReactElement {
const rate = allOperationRequests
? `${toDecimal((operationRequests * 100) / allOperationRequests)}%`
: '-';
return (
<TooltipProvider>
<div className={classes.root}>
<Tooltip>
<TooltipTrigger>
<h2 className={classes.value}>{rate}</h2>
<p className={classes.title}>Total traffic ratio</p>
</TooltipTrigger>
<TooltipContent>
The proportion of traffic accounted for by this operation, taking into account applied
filters, in relation to the total target traffic.
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
<Card className="bg-gray-900/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Relative Request Frequency</CardTitle>
<PercentIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{rate}</div>
<p className="text-xs text-muted-foreground">The impact on the overall API traffic</p>
</CardContent>
</Card>
);
}
function PercentileStats({ value, title }: { value?: number; title: string }): ReactElement {
function PercentileStats({
value,
percentile,
dateRangeText,
}: {
value?: number;
percentile: number;
dateRangeText: string;
}): ReactElement {
const formatted = useFormattedDuration(value);
return (
<div className={classes.root}>
<h2 className={classes.value}>{formatted}</h2>
<p className={classes.title}>{title}</p>
</div>
<Card className="bg-gray-900/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">p{percentile}</CardTitle>
<GaugeIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatted}</div>
<p className="text-xs text-muted-foreground">
Latency p{percentile} in {dateRangeText}
</p>
</CardContent>
</Card>
);
}
function RPM({
period,
dateRangeText,
requests = 0,
}: {
requests?: number;
dateRangeText: string;
period: {
from: string;
to: string;
@ -143,22 +176,31 @@ function RPM({
}): ReactElement {
const throughput = useFormattedThroughput({
requests,
window: new Date(period.to).getTime() - new Date(period.from).getTime(),
window: differenceInMilliseconds(new Date(period.to), new Date(period.from)),
});
return (
<div className={classes.root}>
<h2 className={classes.value}>{throughput}</h2>
<p className={classes.title}>RPM</p>
</div>
<Card className="bg-gray-900/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Requests per minute</CardTitle>
<ActivityIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{throughput}</div>
<p className="text-xs text-muted-foreground">Throughput in {dateRangeText}</p>
</CardContent>
</Card>
);
}
function SuccessRateStats({
requests = 0,
totalFailures = 0,
dateRangeText,
}: {
requests?: number;
totalFailures?: number;
dateRangeText: string;
}): ReactElement {
const rate =
requests || totalFailures
@ -166,27 +208,45 @@ function SuccessRateStats({
: '-';
return (
<div className={classes.root}>
<h2 className={cn(classes.value, 'text-emerald-500 dark:text-emerald-500')}>{rate}</h2>
<p className={classes.title}>Success rate</p>
</div>
<Card className="bg-gray-900/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-emerald-500 dark:text-emerald-500">
Success rate
</CardTitle>
<SmileIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{rate}</div>
<p className="text-xs text-muted-foreground">Successful requests in {dateRangeText}</p>
</CardContent>
</Card>
);
}
function FailureRateStats({
requests = 0,
totalFailures = 0,
dateRangeText,
}: {
requests?: number;
totalFailures?: number;
dateRangeText: string;
}): ReactElement {
const rate = requests || totalFailures ? `${toDecimal((totalFailures * 100) / requests)}%` : '-';
return (
<div className={cn(classes.root)}>
<h2 className={cn(classes.value, 'text-red-500 dark:text-red-500')}>{rate}</h2>
<p className={classes.title}>Failure rate</p>
</div>
<Card className="bg-gray-900/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-red-500 dark:text-red-500">
Failure rate
</CardTitle>
<FrownIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{rate}</div>
<p className="text-xs text-muted-foreground">Failed requests in {dateRangeText}</p>
</CardContent>
</Card>
);
}
@ -497,7 +557,7 @@ function ClientsStats(props: {
(ev: { componentType: string; targetType: string; value: string }) => {
if (ev.componentType === 'yAxis' && ev.targetType === 'axisLabel') {
void router.push({
pathname: '/[organizationId]/[projectId]/[targetId]/operations/client/[name]',
pathname: '/[organizationId]/[projectId]/[targetId]/insights/client/[name]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
@ -957,6 +1017,7 @@ export function OperationsStats({
clientNamesFilter,
resolution,
mode,
dateRangeText,
}: {
organization: string;
project: string;
@ -965,6 +1026,7 @@ export function OperationsStats({
from: string;
to: string;
};
dateRangeText: string;
resolution: number;
operationsFilter: string[];
clientNamesFilter: Array<string>;
@ -1002,31 +1064,54 @@ export function OperationsStats({
const operationsStats = query.data?.operationsStats;
const allOperationsStats = query.data?.allOperations;
dateRangeText = dateRangeText.toLowerCase();
return (
<section className="text-gray-600 dark:text-gray-400 space-y-12 transition-opacity ease-in-out duration-700">
<OperationsFallback isError={isError} refetch={refetch} isFetching={isFetching}>
<div className="grid gap-y-4 grid-cols-4 rounded-md p-5 border border-gray-800 bg-gray-900/50">
<RequestsStats requests={operationsStats?.totalRequests} />
<RPM requests={operationsStats?.totalRequests} period={period} />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<RequestsStats requests={operationsStats?.totalRequests} dateRangeText={dateRangeText} />
<RPM
requests={operationsStats?.totalRequests}
period={period}
dateRangeText={dateRangeText}
/>
{mode === 'operation-list' ? (
<UniqueOperationsStats operations={operationsStats?.totalOperations} />
<UniqueOperationsStats
operations={operationsStats?.totalOperations}
dateRangeText={dateRangeText}
/>
) : (
<OperationRelativeFrequency
allOperationRequests={allOperationsStats?.totalRequests ?? 0}
operationRequests={operationsStats?.totalRequests ?? 0}
dateRangeText={dateRangeText}
/>
)}
<SuccessRateStats
requests={operationsStats?.totalRequests}
totalFailures={operationsStats?.totalFailures}
dateRangeText={dateRangeText}
/>
<PercentileStats
value={operationsStats?.duration?.p99}
percentile={99}
dateRangeText={dateRangeText}
/>
<PercentileStats
value={operationsStats?.duration?.p95}
percentile={95}
dateRangeText={dateRangeText}
/>
<PercentileStats
value={operationsStats?.duration?.p90}
percentile={90}
dateRangeText={dateRangeText}
/>
<PercentileStats value={operationsStats?.duration?.p99} title="Latency p99" />
<PercentileStats value={operationsStats?.duration?.p95} title="Latency p95" />
<PercentileStats value={operationsStats?.duration?.p90} title="Latency p90" />
<FailureRateStats
requests={operationsStats?.totalRequests}
totalFailures={operationsStats?.totalFailures}
dateRangeText={dateRangeText}
/>
</div>
</OperationsFallback>

View file

@ -1,5 +1,4 @@
import { ComponentProps, ReactElement, ReactNode } from 'react';
import { clsx } from 'clsx';
import { Tooltip } from '@/components/v2/tooltip';
import { TriangleUpIcon } from '@radix-ui/react-icons';
import { SortDirection } from '@tanstack/react-table';
@ -23,15 +22,13 @@ export function Sortable({
return (
<Tooltip content={tooltipText}>
<button className="flex gap-2 items-center justify-center" onClick={onClick}>
{children}
<button className="flex items-center justify-center" onClick={onClick}>
<div>{children}</div>
<span>
<TriangleUpIcon className={clsx(sortOrder === 'asc' && 'text-orange-500')} />
<TriangleUpIcon
className={clsx('rotate-180', sortOrder === 'desc' && 'text-orange-500')}
/>
</span>
{sortOrder === 'asc' ? <TriangleUpIcon className="text-orange-500 ml-2" /> : null}
{sortOrder === 'desc' ? (
<TriangleUpIcon className="rotate-180 text-orange-500 ml-2" />
) : null}
</button>
</Tooltip>
);

View file

@ -1,9 +1,9 @@
import { ComponentProps, ReactElement } from 'react';
import clsx from 'clsx';
import { cn } from '@/lib/utils';
function Table({ children, className, ...props }: ComponentProps<'table'>): ReactElement {
return (
<table className={clsx('w-full', className)} {...props}>
<table className={cn('w-full', className)} {...props}>
{children}
</table>
);
@ -31,7 +31,7 @@ function TFoot({ children, ...props }: ComponentProps<'tfoot'>): ReactElement {
function Th({ children, className, align = 'left', ...props }: ComponentProps<'th'>): ReactElement {
return (
<th className={clsx('px-5 py-4', className)} align={align} {...props}>
<th className={cn('px-5 py-4', className)} align={align} {...props}>
{children}
</th>
);
@ -40,7 +40,7 @@ function Th({ children, className, align = 'left', ...props }: ComponentProps<'t
function Tr({ children, className, ...props }: ComponentProps<'tr'>): ReactElement {
return (
<tr
className={clsx('border border-gray-600/10 text-xs odd:bg-gray-600/10', className)}
className={cn('border border-gray-600/10 text-xs odd:bg-gray-600/10', className)}
{...props}
>
{children}
@ -51,8 +51,8 @@ function Tr({ children, className, ...props }: ComponentProps<'tr'>): ReactEleme
function Td({ children, className, ...props }: ComponentProps<'td'>): ReactElement {
return (
<td
className={clsx(
'break-all px-5 py-4 text-sm',
className={cn(
'break-all px-4 py-2 text-sm',
className,
// column.align === 'right' && 'text-right',
// column.width === 'auto' && 'w-1',

View file

@ -116,14 +116,10 @@ performance:
className="mt-10 max-w-2xl drop-shadow-md"
/>
### Operations Overview
### Insights
A list of all the GraphQL operations executed by your consumers, their performance metrics and total
count. By clicking on a specific query, you'll be able to see the full list of fields and arguments
used in the operation.
<NextImage
alt="Operations Overview"
src={usageOperationsImage}
className="mt-10 max-w-2xl drop-shadow-md"
/>
<NextImage alt="Insights" src={usageOperationsImage} className="mt-10 max-w-2xl drop-shadow-md" />