diff --git a/apps/console/src/app/[entity]/dashboard/activity/page.tsx b/apps/console/src/app/[entity]/dashboard/activity/page.tsx new file mode 100644 index 0000000000..c5c07a10a7 --- /dev/null +++ b/apps/console/src/app/[entity]/dashboard/activity/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next"; + +import { generateNextMetaBase } from "@instill-ai/toolkit/server"; + +import ActivityRender from "./render"; + +export async function generateMetadata(): Promise { + return { + title: "Instill Core | Activity Dashboard", + metadataBase: generateNextMetaBase({ + defaultBase: "http://localhost:3000", + }), + openGraph: { + images: ["/instill-open-graph.png"], + }, + }; +} + +export default function ActivityPage() { + return ; +} diff --git a/apps/console/src/app/[entity]/dashboard/activity/render.tsx b/apps/console/src/app/[entity]/dashboard/activity/render.tsx new file mode 100644 index 0000000000..8a5f9a1780 --- /dev/null +++ b/apps/console/src/app/[entity]/dashboard/activity/render.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; + +import { + AppTopbar, + DashboardActivityPageMainView, + NamespaceSwitch, + PageBase, +} from "@instill-ai/toolkit"; + +import { useAppAccessToken } from "~/lib/use-app-access-token"; +import { useAppTrackToken } from "~/lib/useAppTrackToken"; + +export default function ActivityRender() { + useAppAccessToken(); + useAppTrackToken({ enabled: true }); + + return ( + + } /> + + + + + + + ); +} diff --git a/apps/console/src/app/[entity]/dashboard/cost/model/page.tsx b/apps/console/src/app/[entity]/dashboard/cost/model/page.tsx new file mode 100644 index 0000000000..0933c0d6bb --- /dev/null +++ b/apps/console/src/app/[entity]/dashboard/cost/model/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next"; + +import { generateNextMetaBase } from "@instill-ai/toolkit/server"; + +import CostPipelineRender from "./render"; + +export async function generateMetadata(): Promise { + return { + title: "Instill Core | Pipeline Cost Dashboard", + metadataBase: generateNextMetaBase({ + defaultBase: "http://localhost:3000", + }), + openGraph: { + images: ["/instill-open-graph.png"], + }, + }; +} + +export default function CostPipelinePage() { + return ; +} diff --git a/apps/console/src/app/[entity]/dashboard/cost/model/render.tsx b/apps/console/src/app/[entity]/dashboard/cost/model/render.tsx new file mode 100644 index 0000000000..bf5b9c21ba --- /dev/null +++ b/apps/console/src/app/[entity]/dashboard/cost/model/render.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; + +import { + AppTopbar, + DashboardCostModelPageMainView, + NamespaceSwitch, + PageBase, +} from "@instill-ai/toolkit"; + +import { useAppAccessToken } from "~/lib/use-app-access-token"; +import { useAppTrackToken } from "~/lib/useAppTrackToken"; + +export default function CostModelRender() { + useAppAccessToken(); + useAppTrackToken({ enabled: true }); + + return ( + + } /> + + + + + + + ); +} diff --git a/apps/console/src/app/[entity]/dashboard/cost/page.tsx b/apps/console/src/app/[entity]/dashboard/cost/page.tsx new file mode 100644 index 0000000000..af37621c11 --- /dev/null +++ b/apps/console/src/app/[entity]/dashboard/cost/page.tsx @@ -0,0 +1,15 @@ +import { redirect } from "next/navigation"; + +type RedirectionDashboardCostPageProps = { + params: { id: string; entity: string }; +}; + +const RedirectionDashboardCostPage = ({ + params, +}: RedirectionDashboardCostPageProps) => { + const { entity } = params; + + return redirect(`/${entity}/dashboard/cost/pipeline`); +}; + +export default RedirectionDashboardCostPage; diff --git a/apps/console/src/app/[entity]/dashboard/cost/pipeline/page.tsx b/apps/console/src/app/[entity]/dashboard/cost/pipeline/page.tsx new file mode 100644 index 0000000000..6e3e2c1a80 --- /dev/null +++ b/apps/console/src/app/[entity]/dashboard/cost/pipeline/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next"; + +import { generateNextMetaBase } from "@instill-ai/toolkit/server"; + +import CostModelRender from "./render"; + +export async function generateMetadata(): Promise { + return { + title: "Instill Core | Model Cost Dashboard", + metadataBase: generateNextMetaBase({ + defaultBase: "http://localhost:3000", + }), + openGraph: { + images: ["/instill-open-graph.png"], + }, + }; +} + +export default function CostModelPage() { + return ; +} diff --git a/apps/console/src/app/[entity]/dashboard/cost/pipeline/render.tsx b/apps/console/src/app/[entity]/dashboard/cost/pipeline/render.tsx new file mode 100644 index 0000000000..31f3f113ad --- /dev/null +++ b/apps/console/src/app/[entity]/dashboard/cost/pipeline/render.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; + +import { + AppTopbar, + DashboardCostPipelinePageMainView, + NamespaceSwitch, + PageBase, +} from "@instill-ai/toolkit"; + +import { useAppAccessToken } from "~/lib/use-app-access-token"; +import { useAppTrackToken } from "~/lib/useAppTrackToken"; + +export default function CostPipelineRender() { + useAppAccessToken(); + useAppTrackToken({ enabled: true }); + + return ( + + } /> + + + + + + + ); +} diff --git a/apps/console/src/app/[entity]/dashboard/page.tsx b/apps/console/src/app/[entity]/dashboard/page.tsx index d05c0db420..b0e9f45eb5 100644 --- a/apps/console/src/app/[entity]/dashboard/page.tsx +++ b/apps/console/src/app/[entity]/dashboard/page.tsx @@ -1,9 +1,15 @@ import { redirect } from "next/navigation"; -type Props = { - params: { entity: string }; +type RedirectionDashboardPageProps = { + params: { id: string; entity: string }; }; -export default async function Page({ params }: Props) { - redirect(`/${params.entity}/dashboard/pipeline`); -} +const RedirectionDashboardPage = ({ + params, +}: RedirectionDashboardPageProps) => { + const { entity } = params; + + return redirect(`/${entity}/dashboard/activity`); +}; + +export default RedirectionDashboardPage; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 1ebdf82478..1e71d06ac1 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "instill-sdk", - "version": "0.11.0-rc.28", + "version": "0.11.0-rc.37", "description": "Instill AI's Typescript SDK", "repository": "https://github.com/instill-ai/typescript-sdk.git", "bugs": "https://github.com/instill-ai/community/issues", diff --git a/packages/sdk/src/core/metric/MetricClient.ts b/packages/sdk/src/core/metric/MetricClient.ts index 9db605cfb2..7cb14e0d5d 100644 --- a/packages/sdk/src/core/metric/MetricClient.ts +++ b/packages/sdk/src/core/metric/MetricClient.ts @@ -1,7 +1,14 @@ -import { getQueryString } from "../../helper"; +import { getInstillAdditionalHeaders, getQueryString } from "../../helper"; import { APIResource } from "../../main/resource"; +import { PipelineRun } from "../../vdp"; import { ListCreditConsumptionChartRecordResponse, + ListModelTriggerCountRequest, + ListModelTriggerCountResponse, + ListModelTriggerMetricRequest, + ListModelTriggerMetricResponse, + ListPipelineRunsByRequesterRequest, + ListPipelineRunsByRequesterResponse, ListPipelineTriggerComputationTimeChartsRequest, ListPipelineTriggerComputationTimeChartsResponse, ListPipelineTriggerMetricRequest, @@ -15,12 +22,12 @@ import { export class MetricClient extends APIResource { async listInstillCreditConsumptionTimeChart({ - owner, + namespaceId, start, stop, aggregationWindow, }: { - owner: string; + namespaceId: string; start?: string; stop?: string; aggregationWindow?: string; @@ -28,7 +35,7 @@ export class MetricClient extends APIResource { try { const queryString = getQueryString({ baseURL: `/metrics/credit/charts`, - owner, + namespaceId, start: start ?? undefined, stop: stop ?? undefined, aggregationWindow: aggregationWindow ?? undefined, @@ -45,6 +52,129 @@ export class MetricClient extends APIResource { } } + async listModelTriggerMetric( + props: ListModelTriggerMetricRequest & { + enablePagination?: boolean; + }, + ) { + const { + pageSize, + page, + filter, + enablePagination, + requesterId, + requesterUid, + start, + } = props; + const additionalHeaders = getInstillAdditionalHeaders({ + requesterUid, + }); + + try { + const queryString = getQueryString({ + baseURL: `/model-runs/query-charts`, + pageSize, + page, + filter, + requesterId, + start, + }); + + const response = await this._client.get( + queryString, + { + additionalHeaders, + }, + ); + + if (enablePagination) { + return Promise.resolve(response); + } + + return Promise.resolve(response); + } catch (error) { + return Promise.reject(error); + } + } + + async listModelTriggerCount( + request: ListModelTriggerCountRequest, + ): Promise { + const { requesterId, start, stop } = request; + + if (!requesterId) { + return Promise.reject(new Error("requesterId is required")); + } + + try { + const queryString = getQueryString({ + baseURL: `/model-runs/count?`, + requesterId, + start: start ?? undefined, + stop: stop ?? undefined, + }); + + const data = + await this._client.get(queryString); + + return Promise.resolve(data); + } catch (error) { + return Promise.reject(error); + } + } + + async listPipelineRunsByRequester( + props: ListPipelineRunsByRequesterRequest & { enablePagination: true }, + ): Promise; + + async listPipelineRunsByRequester( + props: ListPipelineRunsByRequesterRequest & { enablePagination: false }, + ): Promise; + + async listPipelineRunsByRequester( + props: ListPipelineRunsByRequesterRequest & { enablePagination?: boolean }, + ): Promise { + const { + pageSize, + page, + orderBy, + enablePagination, + requesterUid, + requesterId, + start, + } = props; + + const additionalHeaders = getInstillAdditionalHeaders({ + requesterUid, + }); + + try { + const queryString = getQueryString({ + baseURL: `/dashboard/pipelines/runs`, + pageSize, + page, + orderBy, + requesterId, + start, + }); + + const data = await this._client.get( + queryString, + { + additionalHeaders, + }, + ); + + if (enablePagination) { + return Promise.resolve(data); + } + + return Promise.resolve(data.pipelineRuns); + } catch (error) { + return Promise.reject(error); + } + } + async listPipelineTriggers( props: ListPipelineTriggerRequest & { enablePagination: true; diff --git a/packages/sdk/src/core/metric/types.ts b/packages/sdk/src/core/metric/types.ts index bf5404e8fb..4ffa4c89be 100644 --- a/packages/sdk/src/core/metric/types.ts +++ b/packages/sdk/src/core/metric/types.ts @@ -1,9 +1,44 @@ -import { PipelineMode, PipelineReleaseState } from "../../vdp"; +import { ModelReleaseStage, ModelTriggerStatus } from "../../model"; +import { Nullable } from "../../types"; +import { PipelineMode, PipelineReleaseState, PipelineRun } from "../../vdp"; + +export type Mode = "MODE_UNSPECIFIED" | "MODE_SYNC" | "MODE_ASYNC"; + +export type PipelineTriggerStatus = + | "STATUS_UNSPECIFIED" + | "STATUS_COMPLETED" + | "STATUS_ERRORED"; + +export type PipelineTriggerCount = { + triggerCount: number; + status?: PipelineTriggerStatus; +}; + +export type PipelinesChart = { + pipelineId: string; + pipelineUid: string; + triggerMode: PipelineMode; + status: PipelineTriggerStatus; + timeBuckets: string[]; + triggerCounts: number[] | string[]; + computeTimeDuration: number[] | string[]; + watchState?: PipelineReleaseState; +}; + +export type TriggeredPipeline = { + pipelineId: string; + pipelineUid: string; + triggerCountCompleted: string; + triggerCountErrored: string; + watchState?: PipelineReleaseState; +}; export type CreditConsumptionChartRecord = { - creditOwner: string; + namespaceId: string; timeBuckets: string[]; amount: number[]; + source: string; + creditOwner: string; }; export type ListCreditConsumptionChartRecordResponse = { @@ -11,11 +46,6 @@ export type ListCreditConsumptionChartRecordResponse = { totalAmount: number; }; -export type PipelineTriggerStatus = - | "STATUS_UNSPECIFIED" - | "STATUS_COMPLETED" - | "STATUS_ERRORED"; - export type PipelineTriggerRecord = { triggerTime: string; pipelineTriggerId: string; @@ -25,31 +55,29 @@ export type PipelineTriggerRecord = { computeTimeDuration: number; status: PipelineTriggerStatus; }; - export type ListPipelineTriggerRequest = { pageSize?: number; pageToken?: string; filter?: string; }; - export type ListPipelineTriggersResponse = { pipelineTriggerRecords: PipelineTriggerRecord[]; nextPageToken: string; totalSize: number; }; - export type ListPipelineTriggerMetricRequest = { pageSize?: number; pageToken?: string; filter?: string; }; - export type PipelineTriggerTableRecord = { pipelineId: string; pipelineUid: string; triggerCountCompleted: string; triggerCountErrored: string; watchState?: PipelineReleaseState; + pipelineReleaseId?: string; + pipelineReleaseUid?: string; }; export type ListPipelineTriggerMetricResponse = { @@ -57,13 +85,11 @@ export type ListPipelineTriggerMetricResponse = { nextPageToken: string; totalSize: number; }; - export type ListPipelineTriggerComputationTimeChartsRequest = { pageSize?: number; pageToken?: string; filter?: string; }; - export type PipelineTriggerChartRecord = { pipelineId: string; pipelineUid: string; @@ -72,9 +98,72 @@ export type PipelineTriggerChartRecord = { timeBuckets: string[]; triggerCounts: number[]; computeTimeDuration: number[]; - watchState?: PipelineReleaseState; + pipelineReleaseId?: string; + pipelineReleaseUid?: string; }; export type ListPipelineTriggerComputationTimeChartsResponse = { pipelineTriggerChartRecords: PipelineTriggerChartRecord[]; }; + +export type ModelTriggerTableRecord = { + timeBuckets?: string[]; + triggerCounts: number[]; + requesterId: string; +}; + +export type ListModelTriggerMetricRequest = { + pageSize?: number; + page?: Nullable; + filter?: string; + requesterUid?: string; + requesterId?: Nullable; + start?: string; +}; + +export type ListModelTriggerMetricResponse = { + modelTriggerChartRecords: ModelTriggerTableRecord[]; +}; + +export type ModelMode = "MODE_UNSPECIFIED" | "MODE_SYNC" | "MODE_ASYNC"; + +export type ModelTriggerChartRecord = { + modelId: string; + modelUid: string; + triggerMode?: ModelMode; + status?: ModelTriggerStatus; + timeBuckets?: string[]; + triggerCounts?: number[]; + computeTimeDuration?: number[]; + watchState?: ModelReleaseStage; +}; + +export type ListModelTriggerCountRequest = { + requesterId: string; + start?: string; + stop?: string; +}; + +export type ModelTriggerCountRecord = { + triggerCount: number; + status: ModelTriggerStatus; +}; + +export type ListModelTriggerCountResponse = { + modelTriggerCounts: ModelTriggerCountRecord[]; +}; + +export type ListPipelineRunsByRequesterResponse = { + pipelineRuns: PipelineRun[]; + nextPageToken: string; + totalSize: number; +}; + +export type ListPipelineRunsByRequesterRequest = { + pageSize?: number; + page: Nullable; + orderBy?: string; + requesterUid?: string; + requesterId?: string; + start?: string; +}; diff --git a/packages/sdk/src/helper/getQueryString.ts b/packages/sdk/src/helper/getQueryString.ts index 3527b4a4c6..5141010cdc 100644 --- a/packages/sdk/src/helper/getQueryString.ts +++ b/packages/sdk/src/helper/getQueryString.ts @@ -11,6 +11,8 @@ export const getQueryString = ({ page, visibility, owner, + namespaceId, + requesterId, start, stop, aggregationWindow, @@ -30,6 +32,8 @@ export const getQueryString = ({ // Just pure query params, the function will handle tialing '&' queryParams?: string; owner?: string; + namespaceId?: string; + requesterId?: Nullable; start?: string; stop?: string; aggregationWindow?: string; @@ -100,6 +104,14 @@ export const getQueryString = ({ url += `owner=${owner}&`; } + if (namespaceId) { + url += `namespaceId=${namespaceId}&`; + } + + if (requesterId) { + url += `requesterId=${requesterId}&`; + } + if (start) { url += `start=${start}&`; } diff --git a/packages/sdk/src/model/ModelClient.ts b/packages/sdk/src/model/ModelClient.ts index 633f97f27e..ff29a44dea 100644 --- a/packages/sdk/src/model/ModelClient.ts +++ b/packages/sdk/src/model/ModelClient.ts @@ -17,6 +17,8 @@ import { ListAvailableRegionResponse, ListModelDefinitionsRequest, ListModelDefinitionsResponse, + ListModelRunsByRequesterRequest, + ListModelRunsByRequesterResponse, ListModelRunsRequest, ListModelRunsResponse, ListModelsRequest, @@ -27,6 +29,7 @@ import { ListNamespaceModelVersionsResponse, Model, ModelDefinition, + ModelRun, ModelVersion, PublishNamespaceModelRequest, PublishNamespaceModelResponse, @@ -207,6 +210,58 @@ export class ModelClient extends APIResource { } } + async listModelRunsByRequester( + props: ListModelRunsByRequesterRequest & { enablePagination: true }, + ): Promise; + + async listModelRunsByRequester( + props: ListModelRunsByRequesterRequest & { enablePagination: false }, + ): Promise; + + async listModelRunsByRequester( + props: ListModelRunsByRequesterRequest & { enablePagination?: boolean }, + ): Promise { + const { + pageSize, + page, + orderBy, + enablePagination, + requesterUid, + requesterId, + start, + } = props; + + const additionalHeaders = getInstillAdditionalHeaders({ + requesterUid, + }); + + try { + const queryString = getQueryString({ + baseURL: `/dashboard/models/runs`, + pageSize, + page, + orderBy, + requesterId, + start, + }); + + const data = await this._client.get( + queryString, + { + additionalHeaders, + }, + ); + + if (enablePagination) { + return Promise.resolve(data); + } + + return Promise.resolve(data.runs); + } catch (error) { + return Promise.reject(error); + } + } + async getNamespaceModel({ namespaceId, modelId, diff --git a/packages/sdk/src/model/types.ts b/packages/sdk/src/model/types.ts index 30a57ed71c..ac7b90bec2 100644 --- a/packages/sdk/src/model/types.ts +++ b/packages/sdk/src/model/types.ts @@ -196,6 +196,7 @@ export type ModelRun = { uid: string; modelUid: string; runnerId: string; + namespaceId: string; status: RunStatus; source: RunSource; totalDuration: number; @@ -207,6 +208,7 @@ export type ModelRun = { taskOutputs: GeneralRecord[]; creditAmount: Nullable; requesterId: string; + modelId?: string; }; export type ListModelRunsResponse = { @@ -392,4 +394,36 @@ export type GetNamespaceModelVersionOperationResultResponse = { operation: Nullable; }; +export type ModelTriggerStatus = + | "STATUS_UNSPECIFIED" + | "STATUS_COMPLETED" + | "STATUS_ERRORED"; + +export type ModelTriggersStatusSummaryItem = { + statusType: ModelTriggerStatus; + amount: number; + type: "pipeline" | "model"; + delta: number; +}; + +export type ModelTriggersStatusSummary = { + completed: ModelTriggersStatusSummaryItem; + errored: ModelTriggersStatusSummaryItem; +}; + export type ModelsWatchState = Record>; + +export type ListModelRunsByRequesterRequest = { + pageSize?: number; + page: Nullable; + orderBy?: string; + requesterUid?: string; + requesterId?: string; + start?: string; +}; + +export type ListModelRunsByRequesterResponse = { + runs: ModelRun[]; + nextPageToken: string; + totalSize: number; +}; diff --git a/packages/sdk/src/vdp/pipeline/PipelineClient.test.ts b/packages/sdk/src/vdp/pipeline/PipelineClient.test.ts index 643b0a7716..0b2e9cde3f 100644 --- a/packages/sdk/src/vdp/pipeline/PipelineClient.test.ts +++ b/packages/sdk/src/vdp/pipeline/PipelineClient.test.ts @@ -21,7 +21,7 @@ test("listAccessiblePipelines", async () => { pageSize: 10, pageToken: "pt", filter: "filter", - showDeleted: false, + showDeleted: true, visibility: "VISIBILITY_PUBLIC", orderBy: "order_by", view: "VIEW_FULL", @@ -47,7 +47,7 @@ test("listNamespacePipelines", async () => { pageToken: "pt", view: "VIEW_FULL", filter: "filter", - showDeleted: false, + showDeleted: true, visibility: "VISIBILITY_PUBLIC", orderBy: "order_by", }); diff --git a/packages/sdk/src/vdp/trigger/types.ts b/packages/sdk/src/vdp/trigger/types.ts index 1cc8d1fced..a0354f46d7 100644 --- a/packages/sdk/src/vdp/trigger/types.ts +++ b/packages/sdk/src/vdp/trigger/types.ts @@ -11,6 +11,8 @@ import { PipelineTrace } from "../pipeline"; import { ResourceView, RunSource, RunStatus } from "../types"; export type PipelineRun = { + namespaceId: string; + pipelineId: string; pipelineUid: string; pipelineRunUid: string; pipelineVersion: string; diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index b1ae5e140a..eec4113b87 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@instill-ai/toolkit", - "version": "0.110.0-rc.77", + "version": "0.110.0-rc.112", "description": "Instill AI's frontend toolkit", "repository": "https://github.com/instill-ai/design-system.git", "bugs": "https://github.com/instill-ai/design-system/issues", diff --git a/packages/toolkit/src/components/top-bar/NamespaceSwitch.tsx b/packages/toolkit/src/components/top-bar/NamespaceSwitch.tsx index 89b51c8a9d..4eb7e835b7 100644 --- a/packages/toolkit/src/components/top-bar/NamespaceSwitch.tsx +++ b/packages/toolkit/src/components/top-bar/NamespaceSwitch.tsx @@ -97,7 +97,7 @@ export const NamespaceSwitch = () => { namespaceId: routeInfo.isSuccess ? routeInfo.data.namespaceId : null, accessToken, enabled: enabledQuery && pathnameEvaluator.isModelPlaygroundPage(pathname), - view: "VIEW_BASIC", + view: "VIEW_FULL", }); const namespacesWithRemainingCredit = React.useMemo(() => { diff --git a/packages/toolkit/src/components/top-bar/RemainingCredit.tsx b/packages/toolkit/src/components/top-bar/RemainingCredit.tsx index 71b4b1dc08..42bf1dd0b2 100644 --- a/packages/toolkit/src/components/top-bar/RemainingCredit.tsx +++ b/packages/toolkit/src/components/top-bar/RemainingCredit.tsx @@ -44,10 +44,12 @@ export const RemainingCreditCTA = ({ accessToken, }); + console.log("remainingCredit", remainingCredit.data); + return (
{remainingCredit.isSuccess ? ( -

{`${remainingCredit.data.total.toLocaleString("en-US", { style: "decimal" })} credits left`}

+

{`${remainingCredit.data.total?.toLocaleString("en-US", { style: "decimal" })} credits left`}

) : ( )} diff --git a/packages/toolkit/src/lib/dashboard/generateModelTriggerChartRecordData.ts b/packages/toolkit/src/lib/dashboard/generateModelTriggerChartRecordData.ts new file mode 100644 index 0000000000..2ebae761ed --- /dev/null +++ b/packages/toolkit/src/lib/dashboard/generateModelTriggerChartRecordData.ts @@ -0,0 +1,57 @@ +import { ModelTriggerTableRecord } from "instill-sdk"; + +import { getDateRange } from "./getDateRange"; +import { sortByDate } from "./sortByDate"; + +export function generateModelTriggerChartRecordData( + apiResponse: ModelTriggerTableRecord[], + range: string, +): { xAxis: string[]; yAxis: number[] } { + if (apiResponse.length === 0) { + return { xAxis: [], yAxis: [] }; + } + + // Assert that model is not undefined + const model = apiResponse[0]!; + + // Provide a default empty array if timeBuckets is undefined + const timeBuckets = model.timeBuckets ?? []; + + // Format the time buckets + const normalizedTimeBuckets = timeBuckets.map((bucket) => bucket); + + // Sort and deduplicate xAxis + const xAxisSortedDates = sortByDate([ + ...getDateRange(range), + ...normalizedTimeBuckets, + ]); + + const xAxis = Array.from(new Set(xAxisSortedDates)); + + // Initialize yAxis with zeros + const yAxis = new Array(xAxis.length).fill(0); + + // Populate yAxis with trigger counts + timeBuckets.forEach((bucket, index) => { + const xAxisIndex = xAxis.findIndex((date) => date === bucket); + if (xAxisIndex !== -1) { + yAxis[xAxisIndex] += model.triggerCounts?.[index] ?? 0; + } + }); + + return { + xAxis: xAxis.map((x) => + range === "24h" + ? new Date(x).toLocaleString("en-US", { + day: "numeric", + hour: "numeric", + minute: "numeric", + }) + : new Date(x).toLocaleString("en-US", { + month: "short", + day: "2-digit", + }), + ), + yAxis, + }; +} diff --git a/packages/toolkit/src/lib/dashboard/generatePipelinesChartData.ts b/packages/toolkit/src/lib/dashboard/generatePipelinesChartData.ts new file mode 100644 index 0000000000..ca669128f8 --- /dev/null +++ b/packages/toolkit/src/lib/dashboard/generatePipelinesChartData.ts @@ -0,0 +1,68 @@ +import { PipelinesChart } from "instill-sdk"; + +import { getDateRange } from "./getDateRange"; +import { sortByDate } from "./sortByDate"; + +export type YAxisData = { + name: string; + type: string; + smooth: boolean; + data: number[]; +}; + +export function generatePipelineChartData( + chart: PipelinesChart[], + range: string, +): { xAxis: string[]; yAxis: YAxisData[] } { + // Preprocess and format time bucket dates + const normalizedTimeBuckets: string[][] = chart.map((pipeline) => { + return pipeline.timeBuckets.map((bucket) => bucket); + }); + + const xAxisSortedDates = sortByDate([ + ...getDateRange(range), + ...normalizedTimeBuckets.flat(), + ]); + + console.log("xAxisSortedDates", xAxisSortedDates); + + const xAxis = Array.from(new Set(xAxisSortedDates)); + + const accumulatedTriggerCounts = new Array(xAxis.length).fill(0); + + // Accumulate trigger counts + for (const pipeline of chart) { + for (const [bucketIndex, bucket] of pipeline.timeBuckets.entries()) { + const xAxisIndex = xAxis.findIndex((date) => date === bucket); + if (xAxisIndex !== -1) { + accumulatedTriggerCounts[xAxisIndex] += + pipeline.triggerCounts[bucketIndex]; + } + } + } + + console.log("accumulatedTriggerCounts", accumulatedTriggerCounts); + + return { + xAxis: xAxis.map((x) => + range === "24h" + ? new Date(x).toLocaleString("en-US", { + day: "numeric", + hour: "numeric", + minute: "numeric", + }) + : new Date(x).toLocaleString("en-US", { + month: "short", + day: "2-digit", + }), + ), + yAxis: [ + { + name: "allPipelineTrigger", + type: "line", + smooth: true, + data: accumulatedTriggerCounts, + }, + ], + }; +} diff --git a/packages/toolkit/src/lib/dashboard/getDateRange.ts b/packages/toolkit/src/lib/dashboard/getDateRange.ts index 5614e3d5ed..164ba77428 100644 --- a/packages/toolkit/src/lib/dashboard/getDateRange.ts +++ b/packages/toolkit/src/lib/dashboard/getDateRange.ts @@ -2,11 +2,11 @@ export function getDateRange(range: string): string[] { const today = new Date(); const dates: string[] = []; - if (range === "1d") { + if (range === "1d" || range === "24h") { const startDate = new Date( today.getFullYear(), today.getMonth(), - today.getDate() - 1, + today.getDate(), 0, 0, 0, @@ -15,9 +15,9 @@ export function getDateRange(range: string): string[] { today.getFullYear(), today.getMonth(), today.getDate(), - 0, - 0, - 0, + 23, + 59, + 59, ); for ( @@ -25,57 +25,10 @@ export function getDateRange(range: string): string[] { date < endDate; date.setHours(date.getHours() + 1) ) { - dates.push( - date.toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - }), - ); - } - // push end date - dates.push( - endDate.toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - }), - ); - } else if (range === "24h") { - const startDate = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate(), - 0, - 0, - 0, - ); - - for ( - let date = startDate; - date <= today; - date.setHours(date.getHours() + 1) - ) { - dates.push( - date.toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - }), - ); + dates.push(date.toISOString().split(".")[0] + "Z"); } // push end date - dates.push( - today.toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - }), - ); + dates.push(endDate.toISOString().split(".")[0] + "Z"); } else if (range.endsWith("d")) { const days = parseInt(range.slice(0, -1)); const startDate = new Date(today.getTime() - days * 24 * 60 * 60 * 1000); @@ -85,9 +38,7 @@ export function getDateRange(range: string): string[] { date <= today; date.setDate(date.getDate() + 1) ) { - dates.push( - date.toLocaleDateString("en-US", { month: "short", day: "numeric" }), - ); + dates.push(date.toISOString().split(".")[0] + "Z"); } } else { throw new Error( diff --git a/packages/toolkit/src/lib/dashboard/getModelTriggersSummary.ts b/packages/toolkit/src/lib/dashboard/getModelTriggersSummary.ts new file mode 100644 index 0000000000..2b2a7db1e9 --- /dev/null +++ b/packages/toolkit/src/lib/dashboard/getModelTriggersSummary.ts @@ -0,0 +1,28 @@ +import { ModelTriggerCountRecord, ModelTriggersStatusSummary } from "instill-sdk"; + +export function getModelTriggersSummary( + modelTriggerCounts: ModelTriggerCountRecord[] +): ModelTriggersStatusSummary { + const completedModel = modelTriggerCounts.find( + (r) => r.status === "STATUS_COMPLETED" + ); + + const erroredModel = modelTriggerCounts.find( + (r) => r.status === "STATUS_ERRORED" + ); + + return { + completed: { + statusType: "STATUS_COMPLETED" as const, + type: "model" as const, + amount: completedModel?.triggerCount || 0, + delta: 0 + }, + errored: { + statusType: "STATUS_ERRORED" as const, + type: "model" as const, + amount: erroredModel?.triggerCount || 0, + delta: 0 + } + }; +} \ No newline at end of file diff --git a/packages/toolkit/src/lib/dashboard/getTimeInRFC3339Format.ts b/packages/toolkit/src/lib/dashboard/getTimeInRFC3339Format.ts index 7138cc2c8e..560b0cc3fd 100644 --- a/packages/toolkit/src/lib/dashboard/getTimeInRFC3339Format.ts +++ b/packages/toolkit/src/lib/dashboard/getTimeInRFC3339Format.ts @@ -17,6 +17,12 @@ export function getTimeInRFC3339Format(interval: string): string { return today.toISOString().split(".")[0] + "Z"; } + if (interval === "todayEnd") { + const today = new Date(); + today.setHours(23, 59, 59, 999); // Set time to 11:59 PM + return today.toISOString().split(".")[0] + "Z"; + } + if (!match || !match[1]) { throw new Error( "Invalid time interval format. Supported formats are: now, todayStart, 1h, 3h, 6h, 24h, 1d, 7d", diff --git a/packages/toolkit/src/lib/dashboard/index.ts b/packages/toolkit/src/lib/dashboard/index.ts index 418359bc48..e42b39ba77 100644 --- a/packages/toolkit/src/lib/dashboard/index.ts +++ b/packages/toolkit/src/lib/dashboard/index.ts @@ -3,6 +3,9 @@ export * from "./convertTimestampToLocal"; export * from "./convertToSecondsAndMilliseconds"; export * from "./formatDateTime"; export * from "./getDateRange"; +export * from "./generateModelTriggerChartRecordData"; +export * from "./generatePipelinesChartData"; +export * from "./getModelTriggersSummary"; export * from "./generateChartData"; export * from "./getPipeLineOptions"; export * from "./getPipelineTriggersSummary"; diff --git a/packages/toolkit/src/lib/dashboard/options.ts b/packages/toolkit/src/lib/dashboard/options.ts index deb02fa8c8..864073c5e2 100644 --- a/packages/toolkit/src/lib/dashboard/options.ts +++ b/packages/toolkit/src/lib/dashboard/options.ts @@ -5,10 +5,6 @@ const timeLineOptions: SelectOption[] = [ label: "Today", value: "24h", }, - { - label: "Last day", - value: "1d", - }, { label: "7 days", value: "7d", diff --git a/packages/toolkit/src/lib/react-query-service/metric/index.ts b/packages/toolkit/src/lib/react-query-service/metric/index.ts index 3f9080f5e5..e4f1310ead 100644 --- a/packages/toolkit/src/lib/react-query-service/metric/index.ts +++ b/packages/toolkit/src/lib/react-query-service/metric/index.ts @@ -2,3 +2,7 @@ export * from "./useCreditConsumptionChartRecords"; export * from "./usePipelineTriggers"; export * from "./usePipelineTriggerMetric"; export * from "./usePipelineTriggerComputationTimeCharts"; +export * from "./useModelTriggerCount"; +export * from "./useModelTriggerMetric"; +export * from "./useListModelRunsByRequester"; +export * from "./useListPipelineRunsByRequester"; diff --git a/packages/toolkit/src/lib/react-query-service/metric/useCreditConsumptionChartRecords.ts b/packages/toolkit/src/lib/react-query-service/metric/useCreditConsumptionChartRecords.ts index 4a8878d4e7..ecec9f095f 100644 --- a/packages/toolkit/src/lib/react-query-service/metric/useCreditConsumptionChartRecords.ts +++ b/packages/toolkit/src/lib/react-query-service/metric/useCreditConsumptionChartRecords.ts @@ -1,7 +1,7 @@ "use client"; -import type { Nullable } from "instill-sdk"; import { useQuery } from "@tanstack/react-query"; +import { Nullable } from "instill-sdk"; import { getInstillAPIClient } from "../../sdk-helper"; @@ -10,45 +10,28 @@ export function useCreditConsumptionChartRecords({ accessToken, start, stop, - owner, + namespaceId, aggregationWindow, }: { enabled: boolean; - owner: Nullable; + namespaceId: Nullable; accessToken: Nullable; start: Nullable; stop: Nullable; aggregationWindow: Nullable; }) { let enabledQuery = false; - if (enabled && start && stop && aggregationWindow) { enabledQuery = true; } - const startDate = start - ? new Date(start).toLocaleString("en-us", { - hour: "2-digit", - month: "2-digit", - day: "2-digit", - }) - : null; - - const stopDate = stop - ? new Date(stop).toLocaleString("en-us", { - hour: "2-digit", - month: "2-digit", - day: "2-digit", - }) - : null; - return useQuery({ queryKey: [ - owner, - "charts", + namespaceId, + "modelTriggerCharts", "creditConsumption", - startDate, - stopDate, + start, + stop, aggregationWindow, ], queryFn: async () => { @@ -56,8 +39,8 @@ export function useCreditConsumptionChartRecords({ return Promise.reject(new Error("accessToken not provided")); } - if (!owner) { - return Promise.reject(new Error("owner not provided")); + if (!namespaceId) { + return Promise.reject(new Error("namespaceId not provided")); } const client = getInstillAPIClient({ @@ -66,7 +49,7 @@ export function useCreditConsumptionChartRecords({ const data = await client.core.metric.listInstillCreditConsumptionTimeChart({ - owner, + namespaceId, start: start ?? undefined, stop: stop ?? undefined, aggregationWindow: aggregationWindow ?? undefined, diff --git a/packages/toolkit/src/lib/react-query-service/metric/useListModelRunsByRequester.ts b/packages/toolkit/src/lib/react-query-service/metric/useListModelRunsByRequester.ts new file mode 100644 index 0000000000..67c3059257 --- /dev/null +++ b/packages/toolkit/src/lib/react-query-service/metric/useListModelRunsByRequester.ts @@ -0,0 +1,60 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { ListModelRunsByRequesterResponse, Nullable } from "instill-sdk"; + +import { getInstillModelAPIClient } from "../../sdk-helper"; + +export function useListModelRunsByRequester({ + enabled, + accessToken, + pageSize, + page, + orderBy, + requesterId, + requesterUid, + start, +}: { + enabled: boolean; + accessToken: Nullable; + pageSize?: number; + page: Nullable; + orderBy?: string; + requesterId?: string; + requesterUid?: string; + start?: string; +}) { + return useQuery({ + queryKey: [ + "modelRuns", + requesterId, + requesterUid, + pageSize, + page, + orderBy, + start, + ], + queryFn: async () => { + if (!accessToken) { + return Promise.reject(new Error("accessToken not provided")); + } + + const client = getInstillModelAPIClient({ + accessToken, + }); + + const data = await client.model.listModelRunsByRequester({ + pageSize, + page, + orderBy, + requesterId, + requesterUid, + start, + enablePagination: true, + }); + + return data; + }, + enabled: enabled, + }); +} diff --git a/packages/toolkit/src/lib/react-query-service/metric/useListPipelineRunsByRequester.ts b/packages/toolkit/src/lib/react-query-service/metric/useListPipelineRunsByRequester.ts new file mode 100644 index 0000000000..08f33b4405 --- /dev/null +++ b/packages/toolkit/src/lib/react-query-service/metric/useListPipelineRunsByRequester.ts @@ -0,0 +1,57 @@ +import { useQuery } from "@tanstack/react-query"; +import { ListPipelineRunsByRequesterResponse, Nullable } from "instill-sdk"; + +import { getInstillAPIClient } from "../../sdk-helper"; + +export function useListPipelineRunsByRequester({ + enabled, + accessToken, + pageSize, + page, + orderBy, + requesterId, + requesterUid, + start, +}: { + enabled: boolean; + accessToken: Nullable; + pageSize?: number; + page: Nullable; + orderBy?: string; + requesterId?: string; + requesterUid?: string; + start: string; +}) { + return useQuery({ + queryKey: [ + "pipelineRuns", + requesterId, + requesterUid, + pageSize, + page, + orderBy, + start, + ], + queryFn: async () => { + if (!accessToken) { + return Promise.reject(new Error("accessToken not provided")); + } + + const client = getInstillAPIClient({ + accessToken, + }); + + const data = await client.core.metric.listPipelineRunsByRequester({ + pageSize, + page, + orderBy, + requesterId, + requesterUid, + start, + enablePagination: true, + }); + return data; + }, + enabled: enabled, + }); +} diff --git a/packages/toolkit/src/lib/react-query-service/metric/useModelTriggerCount.ts b/packages/toolkit/src/lib/react-query-service/metric/useModelTriggerCount.ts new file mode 100644 index 0000000000..a174d6057b --- /dev/null +++ b/packages/toolkit/src/lib/react-query-service/metric/useModelTriggerCount.ts @@ -0,0 +1,40 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { ListModelTriggerCountRequest, Nullable } from "instill-sdk"; +import { getInstillAPIClient } from "../../sdk-helper"; + +export function useModelTriggerCount({ + enabled, + accessToken, + requesterId, + start, + stop, +}: { + enabled: boolean; + accessToken: Nullable; + requesterId: string; + start?: string; + stop?: string; +}) { + return useQuery({ + queryKey: ["modelTriggerCount", requesterId, start, stop], + queryFn: async () => { + if (!accessToken) { + throw new Error("accessToken not provided"); + } + + const client = getInstillAPIClient({ accessToken }); + + const request: ListModelTriggerCountRequest = { + requesterId, + start, + stop, + }; + + const data = await client.core.metric.listModelTriggerCount(request); + return data; + }, + enabled: enabled && !!requesterId, + }); +} diff --git a/packages/toolkit/src/lib/react-query-service/metric/useModelTriggerMetric.ts b/packages/toolkit/src/lib/react-query-service/metric/useModelTriggerMetric.ts new file mode 100644 index 0000000000..02a3eabb9b --- /dev/null +++ b/packages/toolkit/src/lib/react-query-service/metric/useModelTriggerMetric.ts @@ -0,0 +1,43 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { Nullable } from "vitest"; + +import { getInstillAPIClient } from "../../sdk-helper"; + +export function useModelTriggerMetric({ + enabled, + accessToken, + filter, + requesterId, +}: { + enabled: boolean; + accessToken: Nullable; + filter: Nullable; + requesterId: Nullable; +}) { + return useQuery({ + queryKey: ["modelTriggerMetrics", filter, requesterId], + queryFn: async () => { + if (!accessToken) { + throw new Error("accessToken not provided"); + } + + const client = getInstillAPIClient({ accessToken }); + + try { + const response = await client.core.metric.listModelTriggerMetric({ + pageSize: undefined, + filter: filter ?? undefined, + requesterId: requesterId ?? undefined, + enablePagination: false, + }); + + return response.modelTriggerChartRecords || []; + } catch (error) { + throw new Error(`Failed to fetch model trigger metrics: ${error}`); + } + }, + enabled, + }); +} \ No newline at end of file diff --git a/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerComputationTimeCharts.ts b/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerComputationTimeCharts.ts index 59ee96b2af..49c4658679 100644 --- a/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerComputationTimeCharts.ts +++ b/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerComputationTimeCharts.ts @@ -10,13 +10,15 @@ export function usePipelineTriggerComputationTimeCharts({ enabled, accessToken, filter, + requesterId, }: { enabled: boolean; accessToken: Nullable; filter: Nullable; + requesterId?: string; }) { return useQuery({ - queryKey: ["charts", filter], + queryKey: ["pipelineTriggerCharts", filter, requesterId], queryFn: async () => { if (!accessToken) { return Promise.reject(new Error("accessToken not provided")); diff --git a/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerMetric.ts b/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerMetric.ts index 98cd99f011..e1a0f222fb 100644 --- a/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerMetric.ts +++ b/packages/toolkit/src/lib/react-query-service/metric/usePipelineTriggerMetric.ts @@ -10,13 +10,15 @@ export function usePipelineTriggerMetric({ enabled, accessToken, filter, + requesterId, }: { enabled: boolean; accessToken: Nullable; filter: Nullable; + requesterId?: string; }) { return useQuery({ - queryKey: ["tables", filter], + queryKey: ["tables", filter, requesterId], queryFn: async () => { if (!accessToken) { return Promise.reject(new Error("accessToken not provided")); diff --git a/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/client.ts b/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/client.ts index 2170198514..ba47242e50 100644 --- a/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/client.ts +++ b/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/client.ts @@ -25,7 +25,7 @@ export function useNamespaceModel({ } return useQuery({ - queryKey: getUseNamespaceModelQueryKey(namespaceId, modelId), + queryKey: getUseNamespaceModelQueryKey(namespaceId, modelId, view), queryFn: async () => { return await fetchNamespaceModel({ namespaceId, diff --git a/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/server.ts b/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/server.ts index 15a7fd120a..7e5d7ce657 100644 --- a/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/server.ts +++ b/packages/toolkit/src/lib/react-query-service/model/use-namespace-model/server.ts @@ -44,8 +44,15 @@ export async function fetchNamespaceModel({ export function getUseNamespaceModelQueryKey( namespaceId: Nullable, modelId: Nullable, + view: Nullable, ) { - return [namespaceId, "models", modelId]; + const queryKey = [namespaceId, "models", modelId]; + + if (view) { + queryKey.push(view); + } + + return queryKey; } export function prefetchNamespaceModel({ @@ -61,7 +68,7 @@ export function prefetchNamespaceModel({ queryClient: QueryClient; view: Nullable; }) { - const queryKey = getUseNamespaceModelQueryKey(namespaceId, modelId); + const queryKey = getUseNamespaceModelQueryKey(namespaceId, modelId, view); return queryClient.prefetchQuery({ queryKey, diff --git a/packages/toolkit/src/lib/react-query-service/queryKeyStore.ts b/packages/toolkit/src/lib/react-query-service/queryKeyStore.ts index 72b67d8981..82031d0ec1 100644 --- a/packages/toolkit/src/lib/react-query-service/queryKeyStore.ts +++ b/packages/toolkit/src/lib/react-query-service/queryKeyStore.ts @@ -523,14 +523,14 @@ export const mgmtQueryKeyStore = { }: { namespaceIds: string[]; }) { - return [namespaceIds.join(","), "remaining-instill-credit"]; + return [namespaceIds.join(","), "namespaces-remaining-instill-credit"]; }, getUseGetNamespaceRemainingInstillCreditQueryKey({ namespaceId, }: { namespaceId: Nullable; }) { - return [namespaceId, "remaining-instill-credit"]; + return [namespaceId, "namespace-remaining-instill-credit"]; }, getUseAPITokensQueryKey() { return ["tokens"]; diff --git a/packages/toolkit/src/view/dashboard/DashboardPipelineDetailsPageMainView.tsx b/packages/toolkit/src/view/dashboard/DashboardPipelineDetailsPageMainView.tsx index 48e450cea6..ad9c8c2d92 100644 --- a/packages/toolkit/src/view/dashboard/DashboardPipelineDetailsPageMainView.tsx +++ b/packages/toolkit/src/view/dashboard/DashboardPipelineDetailsPageMainView.tsx @@ -17,8 +17,8 @@ import { useRouteInfo, useShallow, } from "../../lib"; +import { PipelineTriggersSummary } from "./activity/PipelineTriggersSummary"; import { FilterByDay } from "./FilterByDay"; -import { PipelineTriggersSummary } from "./PipelineTriggersSummary"; import { PipelineTriggersTable } from "./PipelineTriggersTable"; const selector = (store: InstillStore) => ({ diff --git a/packages/toolkit/src/view/dashboard/DashboardPipelineListPageMainView.tsx b/packages/toolkit/src/view/dashboard/DashboardPipelineListPageMainView.tsx index 083e324730..e50fd6bc35 100644 --- a/packages/toolkit/src/view/dashboard/DashboardPipelineListPageMainView.tsx +++ b/packages/toolkit/src/view/dashboard/DashboardPipelineListPageMainView.tsx @@ -19,10 +19,10 @@ import { usePipelineTriggerMetric, useRouteInfo, } from "../../lib"; +import { PipelineTriggerCountsLineChart } from "./activity/PipelineTriggerCountsLineChart"; +import { PipelineTriggersSummary } from "./activity/PipelineTriggersSummary"; import { DashboardPipelinesTable } from "./DashboardPipelinesTable"; import { FilterByDay } from "./FilterByDay"; -import { PipelineTriggerCountsLineChart } from "./PipelineTriggerCountsLineChart"; -import { PipelineTriggersSummary } from "./PipelineTriggersSummary"; export type DashboardPipelineListPageMainViewProps = GeneralAppPageProp; @@ -85,18 +85,21 @@ export const DashboardPipelineListPageMainView = ( enabled: enableQuery && !!queryString, filter: queryString ? queryString : null, accessToken, + requesterId: routeInfo.data.namespaceId ?? undefined, }); const pipelinesChart = usePipelineTriggerComputationTimeCharts({ enabled: enableQuery && !!queryString, filter: queryString ? queryString : null, accessToken, + requesterId: routeInfo.data.namespaceId ?? undefined, }); const previoustriggeredPipelines = usePipelineTriggerMetric({ enabled: enableQuery && !!queryStringPrevious, filter: queryStringPrevious ? queryStringPrevious : null, accessToken, + requesterId: routeInfo.data.namespaceId ?? undefined, }); // Guard this page @@ -218,6 +221,7 @@ export const DashboardPipelineListPageMainView = ( isLoading={pipelinesChart.isLoading} pipelines={pipelinesChart.isSuccess ? pipelinesChartList : []} selectedTimeOption={selectedTimeOption} + pipelineTriggersSummary={null} />
diff --git a/packages/toolkit/src/view/dashboard/FilterByDay.tsx b/packages/toolkit/src/view/dashboard/FilterByDay.tsx index 10b2c9a39a..9ac549791b 100644 --- a/packages/toolkit/src/view/dashboard/FilterByDay.tsx +++ b/packages/toolkit/src/view/dashboard/FilterByDay.tsx @@ -19,34 +19,32 @@ export const FilterByDay = ({ setSelectedTimeOption, }: FilterProps) => { return ( -
-
+
+
{dashboardOptions.timeLine.map((timeLineOption) => ( ))}
); diff --git a/packages/toolkit/src/view/dashboard/StatusTag.tsx b/packages/toolkit/src/view/dashboard/StatusTag.tsx new file mode 100644 index 0000000000..de2caa4f5a --- /dev/null +++ b/packages/toolkit/src/view/dashboard/StatusTag.tsx @@ -0,0 +1,70 @@ +import { Tag } from "@instill-ai/design-system"; + +type StatusTagProps = { + status: string; +}; + +type FileStatus = + | "NOTSTARTED" + | "WAITING" + | "CONVERTING" + | "CHUNKING" + | "EMBEDDING" + | "COMPLETED" + | "FAILED"; + +type TagVariant = + | "lightNeutral" + | "lightYellow" + | "default" + | "lightGreen" + | "lightRed"; + +const getStatusTag = ( + status: FileStatus, +): { variant: TagVariant; dotColor: string } => { + const statusMap: Record< + FileStatus, + { variant: TagVariant; dotColor: string } + > = { + NOTSTARTED: { + variant: "lightNeutral", + dotColor: "bg-semantic-fg-secondary", + }, + WAITING: { + variant: "lightYellow", + dotColor: "bg-semantic-warning-default", + }, + CONVERTING: { variant: "default", dotColor: "bg-semantic-accent-default" }, + CHUNKING: { variant: "default", dotColor: "bg-semantic-accent-default" }, + EMBEDDING: { variant: "default", dotColor: "bg-semantic-accent-default" }, + COMPLETED: { + variant: "lightGreen", + dotColor: "bg-semantic-success-default", + }, + FAILED: { variant: "lightRed", dotColor: "bg-semantic-error-default" }, + }; + return statusMap[status as FileStatus] || statusMap.NOTSTARTED; +}; + +const formatStatus = (status: string): string => { + if (status.toLowerCase() === "notstarted") { + return "Not Started"; + } + return status.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase()); +}; + +export const StatusTag = ({ status }: StatusTagProps) => { + const { variant, dotColor } = getStatusTag( + status.toUpperCase() as FileStatus, + ); + + return ( + +
+
+ {formatStatus(status)} +
+
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/UsageSwitch.tsx b/packages/toolkit/src/view/dashboard/UsageSwitch.tsx new file mode 100644 index 0000000000..c1db102db3 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/UsageSwitch.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import cn from "clsx"; +import { Nullable } from "instill-sdk"; + +import { ToggleGroup } from "@instill-ai/design-system"; + +type UsageSwitchProps = { + activeTab: "activity" | "cost"; + setActiveTab: (tab: "activity" | "cost") => void; + namespaceId: Nullable; +}; + +export const UsageSwitch = ({ + activeTab, + setActiveTab, + namespaceId, +}: UsageSwitchProps) => { + const router = useRouter(); + const pathname = usePathname(); + + const options = [ + { value: "activity", label: "Activity" }, + { value: "cost", label: "Cost" }, + ]; + + const handleTabChange = (value: string) => { + const tab = value as "activity" | "cost"; + setActiveTab(tab); + + if (tab === "activity") { + router.push(`/${namespaceId}/dashboard/activity`); + } else { + if (pathname.includes("/cost/")) { + const subRoute = pathname.split("/cost/")[1]; + router.push(`/${namespaceId}/dashboard/cost/${subRoute}`); + } else { + router.push(`/${namespaceId}/dashboard/cost/pipeline`); + } + } + }; + + return ( + + {options.map((option) => ( + + {option.label} + + ))} + + ); +}; diff --git a/packages/toolkit/src/view/dashboard/activity/ActivityTab.tsx b/packages/toolkit/src/view/dashboard/activity/ActivityTab.tsx new file mode 100644 index 0000000000..59ea5f4b2f --- /dev/null +++ b/packages/toolkit/src/view/dashboard/activity/ActivityTab.tsx @@ -0,0 +1,72 @@ +import * as React from "react"; +import { + ModelTriggersStatusSummary, + ModelTriggerTableRecord, + Nullable, + PipelinesChart, +} from "instill-sdk"; + +import { SelectOption } from "@instill-ai/design-system"; + +import { PipelineTriggersStatusSummary } from "../../../lib"; +import { FilterByDay } from "../FilterByDay"; +import { ModelsTriggerCountsLineChart } from "./ModelsTriggerCountsLineChart"; +import { PipelineTriggerCountsLineChart } from "./PipelineTriggerCountsLineChart"; + +type ActivityTabProps = { + pipelinesChart: { + isLoading: boolean; + refetch: () => void; + }; + modelsChart: { + isLoading: boolean; + refetch: () => void; + }; + pipelinesChartList: PipelinesChart[]; + modelsChartList: ModelTriggerTableRecord[]; + selectedTimeOption: SelectOption; + setSelectedTimeOption: React.Dispatch>; + pipelineTriggersSummary: Nullable; + modelTriggersSummary: Nullable; +}; + +export const ActivityTab = ({ + pipelinesChart, + modelsChart, + pipelinesChartList, + modelsChartList, + selectedTimeOption, + setSelectedTimeOption, + pipelineTriggersSummary, + modelTriggersSummary, +}: ActivityTabProps) => { + return ( +
+
+ pipelinesChart.refetch()} + selectedTimeOption={selectedTimeOption} + setSelectedTimeOption={setSelectedTimeOption} + /> +
+
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/activity/DashboardActivityPageMainView.tsx b/packages/toolkit/src/view/dashboard/activity/DashboardActivityPageMainView.tsx new file mode 100755 index 0000000000..83158ec146 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/activity/DashboardActivityPageMainView.tsx @@ -0,0 +1,263 @@ +"use client"; + +import * as React from "react"; +import { useRouter } from "next/navigation"; +import { + ModelTriggerTableRecord, + PipelinesChart, + TriggeredPipeline, +} from "instill-sdk"; + +import { SelectOption } from "@instill-ai/design-system"; + +import { + DashboardAvailableTimeframe, + getModelTriggersSummary, + getPipelineTriggersSummary, + getPreviousTimeframe, + getTimeInRFC3339Format, + InstillStore, + Nullable, + useInstillStore, + useModelTriggerCount, + useModelTriggerMetric, + usePipelineTriggerComputationTimeCharts, + usePipelineTriggerMetric, + useRouteInfo, + useShallow, + useUserNamespaces, +} from "../../../lib"; +import { UsageSwitch } from "../UsageSwitch"; +import { ActivityTab } from "./ActivityTab"; + +const selector = (store: InstillStore) => ({ + accessToken: store.accessToken, + enabledQuery: store.enabledQuery, + selectedNamespace: store.navigationNamespaceAnchor, +}); + +export const DashboardActivityPageMainView = () => { + const [selectedTimeOption, setSelectedTimeOption] = + React.useState({ + label: "Today", + value: "24h", + }); + + const { accessToken, enabledQuery, selectedNamespace } = useInstillStore( + useShallow(selector), + ); + + const [activeTab, setActiveTab] = React.useState<"activity" | "cost">( + "activity", + ); + + const router = useRouter(); + + const routeInfo = useRouteInfo(); + const userNamespaces = useUserNamespaces(); + + const targetNamespace = React.useMemo(() => { + if (!userNamespaces.isSuccess || !selectedNamespace) { + return null; + } + + return userNamespaces.data.find( + (namespace) => namespace.id === selectedNamespace, + ); + }, [userNamespaces.isSuccess, userNamespaces.data, selectedNamespace]); + + const queryString = React.useMemo>(() => { + if (!targetNamespace) return null; + + let q = `ownerName='${targetNamespace.name}'`; + + if (selectedTimeOption) { + const start = getTimeInRFC3339Format( + selectedTimeOption.value === "24h" + ? "todayStart" + : selectedTimeOption.value, + ); + const stop = getTimeInRFC3339Format( + selectedTimeOption.value === "1d" ? "todayStart" : "now", + ); + + q += ` AND start='${start}' AND stop='${stop}'`; + } + + return q; + }, [selectedTimeOption, targetNamespace]); + + const queryStringPrevious = React.useMemo>(() => { + if (!targetNamespace) return null; + + let qPrev = `ownerName='${targetNamespace.name}'`; + + if (selectedTimeOption) { + const previousTime = getTimeInRFC3339Format( + getPreviousTimeframe( + selectedTimeOption.value as DashboardAvailableTimeframe, + ), + ); + const start = getTimeInRFC3339Format( + selectedTimeOption.value === "1d" + ? "todayStart" + : selectedTimeOption.value, + ); + + qPrev += ` AND start='${previousTime}' AND stop='${start}'`; + } + + return qPrev; + }, [selectedTimeOption, targetNamespace]); + + const triggeredPipelines = usePipelineTriggerMetric({ + enabled: enabledQuery && !!queryString, + filter: queryString ? queryString : null, + accessToken, + requesterId: selectedNamespace ?? undefined, + }); + + const pipelinesChart = usePipelineTriggerComputationTimeCharts({ + enabled: enabledQuery && !!queryString, + filter: queryString ? queryString : null, + accessToken, + requesterId: selectedNamespace ?? undefined, + }); + + const modelsChart = useModelTriggerCount({ + enabled: enabledQuery && !!queryString, + accessToken, + requesterId: selectedNamespace ?? "", + }); + + const triggeredModels = useModelTriggerMetric({ + enabled: enabledQuery && !!queryString, + accessToken, + requesterId: selectedNamespace ?? undefined, + filter: queryString ? queryString : null, + }); + + const previousTriggeredPipelines = usePipelineTriggerMetric({ + enabled: enabledQuery && !!queryStringPrevious, + filter: queryStringPrevious ? queryStringPrevious : null, + accessToken, + requesterId: selectedNamespace ?? undefined, + }); + + const previousTriggeredModels = useModelTriggerMetric({ + enabled: enabledQuery && !!queryStringPrevious, + accessToken, + requesterId: selectedNamespace ?? undefined, + filter: queryStringPrevious ? queryStringPrevious : null, + }); + + React.useEffect(() => { + if ( + triggeredPipelines.isError || + pipelinesChart.isError || + previousTriggeredPipelines.isError || + triggeredModels.isError || + modelsChart.isError || + previousTriggeredModels.isError + ) { + router.push("/404"); + } + }, [ + router, + triggeredPipelines.isError, + pipelinesChart.isError, + previousTriggeredPipelines.isError, + triggeredModels.isError, + modelsChart.isError, + previousTriggeredModels.isError, + ]); + + const pipelinesChartList = React.useMemo(() => { + if (!pipelinesChart.isSuccess) { + return []; + } + + return pipelinesChart.data.map((pipeline) => ({ + ...pipeline, + })); + }, [pipelinesChart.data, pipelinesChart.isSuccess]); + + const modelChartList = React.useMemo(() => { + if (!triggeredModels.isSuccess || !Array.isArray(triggeredModels.data)) { + return []; + } + + return triggeredModels.data.map((model) => ({ + ...model, + })); + }, [triggeredModels.data, triggeredModels.isSuccess]); + + const triggeredPipelineList = React.useMemo(() => { + if (!triggeredPipelines.isSuccess) return []; + return triggeredPipelines.data; + }, [triggeredPipelines.data, triggeredPipelines.isSuccess]); + + // const triggeredModelList = React.useMemo(() => { + // if (!triggeredModels.isSuccess) return []; + // return triggeredModels.data; + // }, [triggeredModels.data, triggeredModels.isSuccess]); + + const pipelineTriggersSummary = React.useMemo(() => { + if (!previousTriggeredPipelines.isSuccess) return null; + + const triggeredPipelineIdList = triggeredPipelineList.map( + (e) => e.pipelineId, + ); + + return getPipelineTriggersSummary( + triggeredPipelineList, + previousTriggeredPipelines.data.filter((trigger) => + triggeredPipelineIdList.includes(trigger.pipelineId), + ), + ); + }, [ + previousTriggeredPipelines.isSuccess, + previousTriggeredPipelines.data, + triggeredPipelineList, + ]); + + const modelTriggersSummary = React.useMemo(() => { + if ( + !previousTriggeredModels.isSuccess || + !modelsChart.isSuccess || + !modelsChart.data.modelTriggerCounts + ) { + return null; + } + + return getModelTriggersSummary(modelsChart.data.modelTriggerCounts); + }, [ + previousTriggeredModels.isSuccess, + modelsChart.isSuccess, + modelsChart.data?.modelTriggerCounts, + selectedTimeOption, // Add this to ensure refetch on date change + ]); + + return ( +
+
+

Usage

+ +
+ +
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/activity/ModelTriggersSummary.tsx b/packages/toolkit/src/view/dashboard/activity/ModelTriggersSummary.tsx new file mode 100644 index 0000000000..c48a649f49 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/activity/ModelTriggersSummary.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { Fragment, ReactNode } from "react"; + +import { Icons, Skeleton, Tag } from "@instill-ai/design-system"; + +import { Nullable, PipelineTriggersStatusSummaryItem } from "../../../lib"; + +const ModelTriggersSummaryCard = (props: { + summary: Nullable; +}) => { + if (!props.summary) { + return ( +
+
+ +
+
+ +
+
+ ); + } + + const { + summary: { statusType, delta, amount }, + } = props; + + let summaryName: Nullable = null; + + switch (statusType) { + case "STATUS_COMPLETED": { + summaryName = "Completed Triggers"; + break; + } + case "STATUS_ERRORED": { + summaryName = "Error Triggers"; + break; + } + default: { + summaryName = "Unspecific Triggers"; + } + } + + return ( +
+
+ {summaryName} +
+
+
+ {amount} +
+
+ {delta < 0 ? ( + + + {`${delta} %`} + + ) : null} + {delta > 0 ? ( + + + {`${delta} %`} + + ) : null} + {delta === 0 ? ( + {`${delta} %`} + ) : null} +
+
+
+ ); +}; + +export type ModelTriggersSummaryProps = { + children: ReactNode; +}; + +export const ModelTriggersSummary = (props: ModelTriggersSummaryProps) => { + const { children } = props; + + return ( + +
{children}
+
+ ); +}; + +ModelTriggersSummary.Card = ModelTriggersSummaryCard; diff --git a/packages/toolkit/src/view/dashboard/activity/ModelsTriggerCountsLineChart.tsx b/packages/toolkit/src/view/dashboard/activity/ModelsTriggerCountsLineChart.tsx new file mode 100644 index 0000000000..3d8c5617c5 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/activity/ModelsTriggerCountsLineChart.tsx @@ -0,0 +1,246 @@ +"use client"; + +import * as React from "react"; +import * as echarts from "echarts"; +import { + ModelTriggersStatusSummary, + ModelTriggerTableRecord, + Nullable, +} from "instill-sdk"; + +import { Icons, SelectOption, Tooltip } from "@instill-ai/design-system"; + +import { generateModelTriggerChartRecordData } from "../../../lib"; +import { ModelTriggersSummary } from "./ModelTriggersSummary"; + +type ModelsTriggerCountsLineChartProps = { + models: ModelTriggerTableRecord[]; + isLoading: boolean; + selectedTimeOption: SelectOption; + modelTriggersSummary: Nullable; +}; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +function selectGraph(params: any, myChart: echarts.ECharts): void { + myChart.dispatchAction({ + type: "legendSelect", + // legend name + name: params.name as string, + }); +} + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +function unselectGraph(params: any, myChart: echarts.ECharts): void { + for (const legend in params.selected) { + if (legend !== params.name) { + myChart.dispatchAction({ + type: "legendUnSelect", + // legend name + name: legend, + }); + } + } +} + +export const ModelsTriggerCountsLineChart = ({ + isLoading, + models, + selectedTimeOption, + modelTriggersSummary, +}: ModelsTriggerCountsLineChartProps) => { + const chartRef = React.useRef(null); + const { xAxis, yAxis } = React.useMemo( + () => generateModelTriggerChartRecordData(models, selectedTimeOption.value), + [models, selectedTimeOption.value], + ); + + React.useEffect(() => { + if (chartRef.current) { + // Dispose the previous chart instance + echarts.dispose(chartRef.current); // eslint-disable-line + const myChart = echarts.init(chartRef.current, null, { + renderer: "svg", + }); // eslint-disable-line + const option = { + grid: { + left: "50px", + right: "50px", + top: 10, + bottom: 50, + }, + graphic: { + type: "image", + style: { + image: "/images/no-chart-placeholder.svg", + x: "45%", + y: "0%", + width: models.length === 0 ? 225 : 0, + height: models.length === 0 ? 225 : 0, + }, + }, + animation: false, + title: { + show: models.length === 0, + textStyle: { + color: "#1D2433A6", + fontSize: 14, + fontWeight: 500, + fontFamily: "var(--font-ibm-plex-sans)", + fontStyle: "italic", + }, + text: isLoading ? "Loading..." : "No models have been triggered yet", + left: `${isLoading ? "49.5%" : "44.5%"}`, + bottom: 100, + }, + tooltip: { + trigger: "item", + tiggerOn: "click", + backgroundColor: "white", + borderColor: "transparent", + textStyle: { + color: "var(--semantic-fg-primary)", + }, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + formatter: function (params: any) { + const triggerTime = params.name; + const computeTimeDuration = params.value; + return ` +
+
+
${triggerTime}
+
+ + All model triggers  + + ${computeTimeDuration} +
+
+
+ `; + }, + }, + xAxis: { + type: "category", + data: xAxis, + axisLabel: { + fontSize: "10px", + fontFamily: "var(--font-ibm-plex-sans)", + fontStyle: "normal", + fontWeight: "500", + color: "#6B7280", + }, + }, + yAxis: { + type: "value", + minInterval: 1, + axisLabel: { + fontSize: "10px", + fontFamily: "var(--font-ibm-plex-sans)", + fontStyle: "normal", + fontWeight: "500", + color: "#6B7280", + }, + }, + series: [ + { + name: "Model Triggers", + type: "line", + smooth: false, + data: yAxis, + symbol: "circle", + symbolSize: 8, + itemStyle: { + borderColor: "white", + borderWidth: 2, + }, + }, + ], + }; + + myChart.setOption(option, true); + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + myChart.on("legendselectchanged", function (params: any) { + const selected = Object.values(params.selected); + if (selected.filter((select) => !select).length === selected.length) { + myChart.dispatchAction({ + type: "legendAllSelect", + }); + } else { + selectGraph(params, myChart); + unselectGraph(params, myChart); + } + }); + } + }, [isLoading, xAxis, yAxis, models]); + + return ( +
+
+
+
+
+ Number of model triggers +
+ + + +
+ +
+
+ + +
+
+
+ Number of triggers +
+
+ Select any pipeline from the table below to view the + number of model triggers within the last{" "} + {selectedTimeOption.label} +
+
+
+ +
+
+
+
+
+
+ {/* Status */} +
+ + + + +
+
+
+
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/PipelineTriggerCountsLineChart.tsx b/packages/toolkit/src/view/dashboard/activity/PipelineTriggerCountsLineChart.tsx similarity index 69% rename from packages/toolkit/src/view/dashboard/PipelineTriggerCountsLineChart.tsx rename to packages/toolkit/src/view/dashboard/activity/PipelineTriggerCountsLineChart.tsx index da72113bca..e3762aa393 100644 --- a/packages/toolkit/src/view/dashboard/PipelineTriggerCountsLineChart.tsx +++ b/packages/toolkit/src/view/dashboard/activity/PipelineTriggerCountsLineChart.tsx @@ -1,17 +1,22 @@ "use client"; -import type { PipelineTriggerChartRecord } from "instill-sdk"; import * as React from "react"; import * as echarts from "echarts"; +import { Nullable, PipelinesChart } from "instill-sdk"; import { Icons, SelectOption, Tooltip } from "@instill-ai/design-system"; -import { generateChartData } from "../../lib"; +import { + generatePipelineChartData, + PipelineTriggersStatusSummary, +} from "../../../lib"; +import { PipelineTriggersSummary } from "./PipelineTriggersSummary"; type PipelineTriggerCountsLineChartProps = { - pipelines: PipelineTriggerChartRecord[]; + pipelines: PipelinesChart[]; isLoading: boolean; selectedTimeOption: SelectOption; + pipelineTriggersSummary: Nullable; }; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -40,9 +45,10 @@ export const PipelineTriggerCountsLineChart = ({ isLoading, pipelines, selectedTimeOption, + pipelineTriggersSummary, }: PipelineTriggerCountsLineChartProps) => { const chartRef = React.useRef(null); - const { xAxis, yAxis } = generateChartData( + const { xAxis, yAxis } = generatePipelineChartData( pipelines, selectedTimeOption.value, ); @@ -93,27 +99,24 @@ export const PipelineTriggerCountsLineChart = ({ tooltip: { trigger: "item", tiggerOn: "click", - + backgroundColor: "white", + borderColor: "transparent", + textStyle: { + color: "var(--semantic-fg-primary)", + }, /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ formatter: function (params: any) { const triggerTime = params.name; - const pipelineId = params.seriesName; const computeTimeDuration = params.value; return `
-
${triggerTime}
-
-
-
-
-
- ${pipelineId} -
-
-
-
Triggers:
-
${computeTimeDuration}
+
${triggerTime}
+
+ + All pipeline triggers  + + ${computeTimeDuration}
@@ -123,26 +126,40 @@ export const PipelineTriggerCountsLineChart = ({ xAxis: { type: "category", data: xAxisData, + axisTick: { + length: 0, + }, axisLabel: { - fontSize: "14px", + fontSize: "10px", fontFamily: "var(--font-ibm-plex-sans)", fontStyle: "normal", - fontWeight: "600", + fontWeight: "500", color: "#6B7280", }, }, yAxis: { type: "value", minInterval: 1, + axisTick: { + length: 0, + }, axisLabel: { - fontSize: "14px", + fontSize: "10px", fontFamily: "var(--font-ibm-plex-sans)", fontStyle: "normal", - fontWeight: "600", + fontWeight: "500", color: "#6B7280", }, }, - series: seriesData, + series: seriesData.map((series) => ({ + ...series, + symbol: "circle", + symbolSize: 4, + itemStyle: { + borderColor: "white", + borderWidth: 0, + }, + })), }; myChart.setOption(option, true); @@ -165,17 +182,16 @@ export const PipelineTriggerCountsLineChart = ({ return (
-
+
- Number of triggers + Number of pipeline triggers
- {/* Tooltip about the chart */} -
- +
+
@@ -187,11 +203,7 @@ export const PipelineTriggerCountsLineChart = ({
- Number of triggers tooltip -
-
- Select any pipeline from the table below to view the - number of pipeline triggers within the last 7 days. + Number of pipeline triggers
@@ -207,6 +219,23 @@ export const PipelineTriggerCountsLineChart = ({
+ {/* Status */} +
+ + + + +
; @@ -12,14 +13,14 @@ const PipelineTriggersSummaryCard = (props: { if (!props.summary) { return (
- +
- +
); @@ -46,12 +47,12 @@ const PipelineTriggersSummaryCard = (props: { } return ( -
-
+
+
{summaryName}
-
+
{amount}
@@ -91,4 +92,5 @@ export const PipelineTriggersSummary = ( ); }; + PipelineTriggersSummary.Card = PipelineTriggersSummaryCard; diff --git a/packages/toolkit/src/view/dashboard/activity/index.ts b/packages/toolkit/src/view/dashboard/activity/index.ts new file mode 100644 index 0000000000..447ca647ce --- /dev/null +++ b/packages/toolkit/src/view/dashboard/activity/index.ts @@ -0,0 +1 @@ +export * from "./DashboardActivityPageMainView"; diff --git a/packages/toolkit/src/view/dashboard/cost/CostTab.tsx b/packages/toolkit/src/view/dashboard/cost/CostTab.tsx new file mode 100755 index 0000000000..71115f73bb --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/CostTab.tsx @@ -0,0 +1,180 @@ +"use client"; + +import * as React from "react"; +import { usePathname, useRouter } from "next/navigation"; + +import { Icons, Popover, SelectOption } from "@instill-ai/design-system"; + +import { + getDateRange, + InstillStore, + sortByDate, + useCreditConsumptionChartRecords, + useInstillStore, + useShallow, +} from "../../../lib"; +import { FilterByDay } from "../FilterByDay"; +import { options } from "../lib"; +import { formatDateToRFC3339, getStartOfDay } from "../lib/helpers"; +import { CreditCostTrendChart } from "./CreditCostTrendChart"; +import { DashboardListModel } from "./model/DashboardListModel"; +import { DashboardListPipeline } from "./pipeline/DashboardListPipeline"; + +type CostTabProps = { + selectedTimeOption: SelectOption; + setSelectedTimeOption: React.Dispatch>; +}; + +const selector = (store: InstillStore) => ({ + accessToken: store.accessToken, + enabledQuery: store.enabledQuery, + selectedNamespace: store.navigationNamespaceAnchor, +}); + +export const CostTab = ({ + selectedTimeOption, + setSelectedTimeOption, +}: CostTabProps) => { + const router = useRouter(); + const pathname = usePathname(); + const costView = pathname.includes("/cost/model") ? "model" : "pipeline"; + + const { accessToken, enabledQuery, selectedNamespace } = useInstillStore( + useShallow(selector), + ); + + // Calculate start date based on selected time option + const start = React.useMemo(() => { + if (selectedTimeOption.value === "24h") { + // For today, start at 00:00:00.000 + return getStartOfDay(new Date()); + } + + // For other periods, calculate days back and start at 00:00:00.000 + const date = new Date(); + date.setDate(date.getDate() - parseInt(selectedTimeOption.value)); + return getStartOfDay(date); + }, [selectedTimeOption.value]); + + // Calculate stop date (current time with milliseconds) + const stop = React.useMemo(() => { + return formatDateToRFC3339(new Date()); + }, []); + + const creditConsumption = useCreditConsumptionChartRecords({ + enabled: enabledQuery, + accessToken, + namespaceId: selectedNamespace, + start, + stop, + aggregationWindow: + selectedTimeOption.value === "24h" || selectedTimeOption.value === "1d" + ? "1h" + : "24h", + }); + + const chartData = React.useMemo(() => { + const record = creditConsumption.data?.creditConsumptionChartRecords?.find( + (record) => record.source === costView, + ); + if (record) { + if (selectedTimeOption.value === "24h") { + // We fill in missing dates + const normalizedDate = sortByDate([ + ...getDateRange(selectedTimeOption.value), + ...record.timeBuckets, + ]); + + // Make sure we have unique dates + const dates = Array.from(new Set(normalizedDate)); + + const newValues = new Array(dates.length).fill(0); + for (let i = 0; i < dates.length; i++) { + if (record.amount[i]) { + newValues[i] = record.amount[i]; + } + } + + return { + dates, + values: newValues, + }; + } + + return { + dates: record.timeBuckets, + values: record.amount, + }; + } + return { dates: [], values: [] }; + }, [creditConsumption.data, costView]); + + const xAxisFormat: "date" | "hour" = React.useMemo(() => { + if ( + selectedTimeOption.value === "24h" || + selectedTimeOption.value === "1d" + ) { + return "hour"; + } + return "date"; + }, [selectedTimeOption.value]); + + return ( +
+
+
+ + + + + + {options.map((option) => ( + + ))} + + +
+ creditConsumption.refetch()} + selectedTimeOption={selectedTimeOption} + setSelectedTimeOption={setSelectedTimeOption} + /> +
+
+ +
+ {costView === "model" ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/cost/CreditCostTrendChart.tsx b/packages/toolkit/src/view/dashboard/cost/CreditCostTrendChart.tsx new file mode 100644 index 0000000000..626b631e00 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/CreditCostTrendChart.tsx @@ -0,0 +1,223 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + TooltipProps, + XAxis, + YAxis, +} from "recharts"; + +import { + Icons, + Skeleton, + Tooltip as TooltipDS, +} from "@instill-ai/design-system"; + +type CreditCostTrendChartProps = { + dates: string[]; + values: number[]; + isLoading: boolean; + type: "model" | "pipeline"; + xAxisFormat: "date" | "hour"; +}; + +export const CreditCostTrendChart = ({ + dates, + values, + isLoading, + type, + xAxisFormat, +}: CreditCostTrendChartProps) => { + const chartColor = type === "model" ? "#2EC291" : "#3B7AF7"; + + const data = React.useMemo(() => { + return dates.map((date, index) => ({ + date, + value: values[index] ?? 0, + })); + }, [dates, values]); + + const formatXAxis = (value: string) => { + const date = new Date(value); + if (xAxisFormat === "hour") { + return `${date.getHours()}:00`; + } else { + return `${date.getDate()} ${date.toLocaleString("default", { + month: "short", + })}`; + } + }; + + const CustomTooltip = ({ + active, + payload, + label, + }: TooltipProps) => { + if (!active || !payload || !payload.length) return null; + + const date = new Date(label); + const formattedDate = + xAxisFormat === "hour" + ? `${date.getHours()}:00 - ${date.getFullYear()}` + : `${date.getDate()} ${date.toLocaleString("default", { + month: "short", + })}, ${date.getFullYear()} - ${date.toTimeString().split(" ")[0]}`; + + return ( +
+

+ {formattedDate} +

+
+
+

+ {(payload[0]?.value ?? 0).toFixed(2)} credits +

+
+
+ ); + }; + + const LoadingSkeleton = () => ( +
+
+ + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ ); + + return ( +
+
+
+
+
+ Credit Cost Trend +
+ + + +
+ +
+
+ + +
+
+
+ Credit Cost Trend +
+
+ View the trend of credit cost for {type}s over time. +
+
+
+ +
+
+
+
+
+ + View billing details + +
+
+ {isLoading ? ( + + ) : ( + + + + + + + + + + )} +
+
+
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/cost/index.ts b/packages/toolkit/src/view/dashboard/cost/index.ts new file mode 100644 index 0000000000..c62c7a94dc --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/index.ts @@ -0,0 +1,2 @@ +export * from "./model"; +export * from "./pipeline"; diff --git a/packages/toolkit/src/view/dashboard/cost/model/DashboardCostModelPageMainView.tsx b/packages/toolkit/src/view/dashboard/cost/model/DashboardCostModelPageMainView.tsx new file mode 100755 index 0000000000..8dd01d4834 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/model/DashboardCostModelPageMainView.tsx @@ -0,0 +1,38 @@ +"use client"; + +import * as React from "react"; + +import { SelectOption } from "@instill-ai/design-system"; + +import { useRouteInfo } from "../../../../lib"; +import { UsageSwitch } from "../../UsageSwitch"; +import { CostTab } from "../CostTab"; + +export const DashboardCostModelPageMainView = () => { + const [selectedTimeOption, setSelectedTimeOption] = + React.useState({ + label: "Today", + value: "24h", + }); + + const [activeTab, setActiveTab] = React.useState<"activity" | "cost">("cost"); + + const routeInfo = useRouteInfo(); + + return ( +
+
+

Usage

+ +
+ +
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/cost/model/DashboardListModel.tsx b/packages/toolkit/src/view/dashboard/cost/model/DashboardListModel.tsx new file mode 100644 index 0000000000..7bae84f95f --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/model/DashboardListModel.tsx @@ -0,0 +1,283 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { ModelRun } from "instill-sdk"; + +import { + ColumnDef, + DataTable, + PaginationState, +} from "@instill-ai/design-system"; + +import { + RunsTableSortableColHeader, + RunStateLabel, +} from "../../../../components"; +import { + InstillStore, + useInstillStore, + useListModelRunsByRequester, + useShallow, + useUserNamespaces, +} from "../../../../lib"; +import { getHumanReadableStringFromTime } from "../../../../server"; +import { TABLE_PAGE_SIZE } from "../../../pipeline/view-pipeline/constants"; + +const selector = (store: InstillStore) => ({ + accessToken: store.accessToken, + enabledQuery: store.enabledQuery, + navigationNamespaceAnchor: store.navigationNamespaceAnchor, +}); + +type DashboardListModelProps = { + start: string; +}; + +export const DashboardListModel = ({ start }: DashboardListModelProps) => { + const { accessToken, enabledQuery, navigationNamespaceAnchor } = + useInstillStore(useShallow(selector)); + + const userNamespaces = useUserNamespaces(); + const targetNamespace = React.useMemo(() => { + if (!userNamespaces.isSuccess || !navigationNamespaceAnchor) { + return null; + } + + return userNamespaces.data.find( + (namespace) => namespace.id === navigationNamespaceAnchor, + ); + }, [ + userNamespaces.isSuccess, + userNamespaces.data, + navigationNamespaceAnchor, + ]); + + const [orderBy, setOrderBy] = React.useState(); + const [paginationState, setPaginationState] = React.useState( + { + pageIndex: 0, + pageSize: TABLE_PAGE_SIZE, + }, + ); + + const modelRuns = useListModelRunsByRequester({ + enabled: enabledQuery && !!targetNamespace, + accessToken, + pageSize: paginationState.pageSize, + page: paginationState.pageIndex, + orderBy: orderBy, + requesterId: targetNamespace?.id, + requesterUid: targetNamespace?.uid, + start, + }); + + const onSortOrderUpdate = (sortValue: string) => { + setPaginationState((currentValue) => ({ + ...currentValue, + pageIndex: 0, + })); + setOrderBy(sortValue); + }; + + const tableColumns = React.useMemo(() => { + const baseColumns: ColumnDef[] = [ + { + accessorKey: "modelId", + header: () =>
Model ID
, + cell: ({ row }) => { + return ( +
+ + {row.getValue("modelId")} + +
+ ); + }, + }, + { + accessorKey: "uid", + header: () =>
Run ID
, + cell: ({ row }) => { + return ( +
+ + {row.getValue("uid")} + +
+ ); + }, + }, + { + accessorKey: "version", + header: () =>
Version
, + cell: ({ row }) => { + return ( +
+ {row.getValue("version")} +
+ ); + }, + }, + { + accessorKey: "status", + header: () =>
Status
, + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "source", + header: () =>
Source
, + cell: ({ row }) => { + const sourceValue = row.getValue("source") as string; + return ( +
+ {sourceValue === "RUN_SOURCE_CONSOLE" + ? "Web" + : sourceValue === "RUN_SOURCE_API" + ? "API" + : sourceValue} +
+ ); + }, + }, + { + accessorKey: "totalDuration", + header: () => ( + + ), + cell: ({ row }) => { + const duration = row.getValue("totalDuration"); + return ( +
+ {duration ? `${duration}ms` : "-"} +
+ ); + }, + }, + { + accessorKey: "createTime", + header: () => ( + + ), + cell: ({ row }) => { + const createTime = row.getValue("createTime"); + return ( +
+ {createTime + ? getHumanReadableStringFromTime( + createTime as string, + Date.now(), + ) + : "-"} +
+ ); + }, + }, + { + accessorKey: "runnerId", + header: () =>
Runner
, + cell: ({ row }) => { + return ( +
+ + {row.getValue("runnerId")} + +
+ ); + }, + }, + { + accessorKey: "creditAmount", + header: () =>
Credit
, + cell: ({ row }) => { + return ( +
+ {row.getValue("creditAmount")} +
+ ); + }, + }, + { + accessorKey: "requesterId", + header: () =>
Credit Owner
, + cell: ({ row }) => { + return ( +
+ {row.getValue("requesterId")} +
+ ); + }, + }, + ]; + + return baseColumns; + }, [orderBy, targetNamespace?.id]); + + if ( + modelRuns.isSuccess && + (!modelRuns.data?.runs || modelRuns.data.runs.length === 0) + ) { + return ( +
+ A box and a looking glass +

+ No model runs found + + Once you run a model, it will appear here + +

+
+ ); + } + + return ( +
+ +
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/cost/model/index.ts b/packages/toolkit/src/view/dashboard/cost/model/index.ts new file mode 100644 index 0000000000..e595865824 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/model/index.ts @@ -0,0 +1 @@ +export * from "./DashboardCostModelPageMainView"; diff --git a/packages/toolkit/src/view/dashboard/cost/pipeline/DashboardCostPipelinePageMainView.tsx b/packages/toolkit/src/view/dashboard/cost/pipeline/DashboardCostPipelinePageMainView.tsx new file mode 100755 index 0000000000..5bd1a9fbf4 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/pipeline/DashboardCostPipelinePageMainView.tsx @@ -0,0 +1,38 @@ +"use client"; + +import * as React from "react"; + +import { SelectOption } from "@instill-ai/design-system"; + +import { useRouteInfo } from "../../../../lib"; +import { UsageSwitch } from "../../UsageSwitch"; +import { CostTab } from "../CostTab"; + +export const DashboardCostPipelinePageMainView = () => { + const [selectedTimeOption, setSelectedTimeOption] = + React.useState({ + label: "Today", + value: "24h", + }); + + const [activeTab, setActiveTab] = React.useState<"activity" | "cost">("cost"); + + const routeInfo = useRouteInfo(); + + return ( +
+
+

Usage

+ +
+ +
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/cost/pipeline/DashboardListPipeline.tsx b/packages/toolkit/src/view/dashboard/cost/pipeline/DashboardListPipeline.tsx new file mode 100644 index 0000000000..7411841880 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/pipeline/DashboardListPipeline.tsx @@ -0,0 +1,279 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { PipelineRun } from "instill-sdk"; + +import { + ColumnDef, + DataTable, + PaginationState, +} from "@instill-ai/design-system"; + +import { + RunsTableSortableColHeader, + RunStateLabel, +} from "../../../../components"; +import { + InstillStore, + useInstillStore, + useListPipelineRunsByRequester, + useShallow, + useUserNamespaces, +} from "../../../../lib"; +import { getHumanReadableStringFromTime } from "../../../../server"; +import { TABLE_PAGE_SIZE } from "../../../pipeline/view-pipeline/constants"; + +const selector = (store: InstillStore) => ({ + accessToken: store.accessToken, + enabledQuery: store.enabledQuery, + navigationNamespaceAnchor: store.navigationNamespaceAnchor, +}); + +type DashboardListPipelineProps = { + start: string; +}; + +export const DashboardListPipeline = ({ + start, +}: DashboardListPipelineProps) => { + const { accessToken, enabledQuery, navigationNamespaceAnchor } = + useInstillStore(useShallow(selector)); + + const userNamespaces = useUserNamespaces(); + const targetNamespace = React.useMemo(() => { + if (!userNamespaces.isSuccess || !navigationNamespaceAnchor) { + return null; + } + + return userNamespaces.data.find( + (namespace) => namespace.id === navigationNamespaceAnchor, + ); + }, [ + userNamespaces.isSuccess, + userNamespaces.data, + navigationNamespaceAnchor, + ]); + + const [orderBy, setOrderBy] = React.useState(); + const [paginationState, setPaginationState] = React.useState( + { + pageIndex: 0, + pageSize: TABLE_PAGE_SIZE, + }, + ); + + const pipelineRuns = useListPipelineRunsByRequester({ + enabled: enabledQuery && !!targetNamespace, + accessToken, + pageSize: paginationState.pageSize, + page: paginationState.pageIndex, + orderBy: orderBy, + requesterId: targetNamespace?.id, + requesterUid: targetNamespace?.uid, + start, + }); + + const onSortOrderUpdate = (sortValue: string) => { + setPaginationState((currentValue) => ({ + ...currentValue, + pageIndex: 0, + })); + setOrderBy(sortValue); + }; + + const tableColumns = React.useMemo(() => { + const baseColumns: ColumnDef[] = [ + { + accessorKey: "pipelineId", + header: () =>
Pipeline ID
, + cell: ({ row }) => { + console.log(row.original); + return ( +
+ + {row.getValue("pipelineId")} + +
+ ); + }, + }, + { + accessorKey: "pipelineRunUid", + header: () =>
Run ID
, + cell: ({ row }) => { + return ( +
+ + {row.getValue("pipelineRunUid")} + +
+ ); + }, + }, + { + accessorKey: "pipelineVersion", + header: () =>
Version
, + cell: ({ row }) => { + return ( +
+ {row.getValue("pipelineVersion")} +
+ ); + }, + }, + { + accessorKey: "status", + header: () =>
Status
, + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "source", + header: () =>
Source
, + cell: ({ row }) => { + const sourceValue = row.getValue("source") as string; + return ( +
+ {sourceValue === "RUN_SOURCE_CONSOLE" + ? "Web" + : sourceValue === "RUN_SOURCE_API" + ? "API" + : sourceValue} +
+ ); + }, + }, + { + accessorKey: "totalDuration", + header: () => ( + + ), + cell: ({ row }) => { + return ( +
+ {row.getValue("totalDuration")} +
+ ); + }, + }, + { + accessorKey: "startTime", + header: () => ( + + ), + cell: ({ row }) => { + return ( +
+ {getHumanReadableStringFromTime( + row.getValue("startTime"), + Date.now(), + )} +
+ ); + }, + }, + { + accessorKey: "runnerId", + header: () =>
Runner
, + cell: ({ row }) => { + return ( +
+ + {row.getValue("runnerId")} + +
+ ); + }, + }, + { + accessorKey: "creditAmount", + header: () =>
Credit
, + cell: ({ row }) => { + return ( +
+ {row.getValue("creditAmount")} +
+ ); + }, + }, + { + accessorKey: "requesterId", + header: () =>
Credit Owner
, + cell: ({ row }) => { + return ( +
+ {row.getValue("requesterId")} +
+ ); + }, + }, + ]; + + return baseColumns; + }, [orderBy, targetNamespace?.id]); + + if (pipelineRuns.isSuccess && pipelineRuns.data.pipelineRuns.length === 0) { + return ( +
+ A box and a looking glass +

+ No pipeline runs found + + Once you run a pipeline, it will appear here + +

+
+ ); + } + + return ( +
+ +
+ ); +}; diff --git a/packages/toolkit/src/view/dashboard/cost/pipeline/index.ts b/packages/toolkit/src/view/dashboard/cost/pipeline/index.ts new file mode 100644 index 0000000000..fa23432a5a --- /dev/null +++ b/packages/toolkit/src/view/dashboard/cost/pipeline/index.ts @@ -0,0 +1 @@ +export * from "./DashboardCostPipelinePageMainView"; diff --git a/packages/toolkit/src/view/dashboard/index.ts b/packages/toolkit/src/view/dashboard/index.ts index 4b4a2fdd08..3e05ba90c1 100644 --- a/packages/toolkit/src/view/dashboard/index.ts +++ b/packages/toolkit/src/view/dashboard/index.ts @@ -2,3 +2,5 @@ export * from "./DashboardPipelineDetailsPageMainView"; export * from "./DashboardPipelineListPageMainView"; export * from "./SemiCircleProgress"; export * from "./DashboardContainer"; +export * from "./activity"; +export * from "./cost"; diff --git a/packages/toolkit/src/view/dashboard/lib/constant.tsx b/packages/toolkit/src/view/dashboard/lib/constant.tsx new file mode 100644 index 0000000000..b214b23818 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/lib/constant.tsx @@ -0,0 +1,14 @@ +import { Icons } from "@instill-ai/design-system"; + +export const options = [ + { + value: "pipeline", + label: "Pipeline", + icon: , + }, + { + value: "model", + label: "Model", + icon: , + }, +]; diff --git a/packages/toolkit/src/view/dashboard/lib/helpers.ts b/packages/toolkit/src/view/dashboard/lib/helpers.ts new file mode 100644 index 0000000000..7a58afa2e7 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/lib/helpers.ts @@ -0,0 +1,52 @@ +export const columns = [ + { + accessorKey: "pipelineId", + header: "Pipeline ID", + }, + { + accessorKey: "runId", + header: "Run ID", + }, + { + accessorKey: "version", + header: "Version", + }, + { + accessorKey: "status", + header: "Status", + }, + { + accessorKey: "source", + header: "Source", + }, + { + accessorKey: "totalDuration", + header: "Total Duration", + }, + { + accessorKey: "triggerTime", + header: "Trigger Time", + }, + { + accessorKey: "runner", + header: "Runner", + }, + { + accessorKey: "credit", + header: "Credit", + }, + { + accessorKey: "creditOwner", + header: "Credit Owner", + }, +]; + +export const formatDateToRFC3339 = (date: Date) => { + return date.toISOString().split(".")[0] + ".000Z"; +}; + +export const getStartOfDay = (date: Date) => { + const newDate = new Date(date); + newDate.setHours(0, 0, 0, 0); + return formatDateToRFC3339(newDate); +}; diff --git a/packages/toolkit/src/view/dashboard/lib/index.ts b/packages/toolkit/src/view/dashboard/lib/index.ts new file mode 100644 index 0000000000..56881da859 --- /dev/null +++ b/packages/toolkit/src/view/dashboard/lib/index.ts @@ -0,0 +1,2 @@ +export * from "./helpers"; +export * from "./constant"; diff --git a/packages/toolkit/src/view/recipe-editor/lib/schema.ts b/packages/toolkit/src/view/recipe-editor/lib/schema.ts index b5346dd321..96681ef2d5 100644 --- a/packages/toolkit/src/view/recipe-editor/lib/schema.ts +++ b/packages/toolkit/src/view/recipe-editor/lib/schema.ts @@ -45,6 +45,7 @@ const connectorDefinitionIds = [ "collection", "universal-ai", "instill-app", + "google-drive", ]; export const InstillYamlSchema = {