Browse Source

feat(admin-ui-plugin): Add simple metrics support via new metricSummary query

Michael Bromley 2 years ago
parent
commit
717d26527e

+ 1 - 0
packages/admin-ui-plugin/package.json

@@ -28,6 +28,7 @@
         "typescript": "4.9.5"
     },
     "dependencies": {
+        "date-fns": "^2.30.0",
         "fs-extra": "^10.0.0"
     }
 }

+ 33 - 0
packages/admin-ui-plugin/src/api/api-extensions.ts

@@ -0,0 +1,33 @@
+import gql from 'graphql-tag';
+
+export const adminApiExtensions = gql`
+    type MetricSummary {
+        interval: MetricInterval!
+        type: MetricType!
+        title: String!
+        entries: [MetricSummaryEntry!]!
+    }
+    enum MetricInterval {
+        Daily
+    }
+    enum MetricType {
+        OrderCount
+        OrderTotal
+        AverageOrderValue
+    }
+    type MetricSummaryEntry {
+        label: String!
+        value: Float!
+    }
+    input MetricSummaryInput {
+        interval: MetricInterval!
+        types: [MetricType!]!
+        refresh: Boolean
+    }
+    extend type Query {
+        """
+        Get metrics for the given interval and metric types.
+        """
+        metricSummary(input: MetricSummaryInput): [MetricSummary!]!
+    }
+`;

+ 19 - 0
packages/admin-ui-plugin/src/api/metrics.resolver.ts

@@ -0,0 +1,19 @@
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, Permission, RequestContext } from '@vendure/core';
+
+import { MetricsService } from '../service/metrics.service';
+import { MetricSummary, MetricSummaryInput } from '../types';
+
+@Resolver()
+export class MetricsResolver {
+    constructor(private service: MetricsService) {}
+
+    @Query()
+    @Allow(Permission.ReadOrder)
+    async metricSummary(
+        @Ctx() ctx: RequestContext,
+        @Args('input') input: MetricSummaryInput,
+    ): Promise<MetricSummary[]> {
+        return this.service.getMetrics(ctx, input);
+    }
+}

+ 87 - 0
packages/admin-ui-plugin/src/config/metrics-strategies.ts

@@ -0,0 +1,87 @@
+import { RequestContext } from '@vendure/core';
+
+import { MetricData } from '../service/metrics.service';
+import { MetricInterval, MetricSummaryEntry, MetricType } from '../types';
+
+/**
+ * Calculate your metric data based on the given input.
+ * Be careful with heavy queries and calculations,
+ * as this function is executed everytime a user views its dashboard
+ *
+ */
+export interface MetricCalculation {
+    type: MetricType;
+
+    getTitle(ctx: RequestContext): string;
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry;
+}
+
+export function getMonthName(monthNr: number): string {
+    const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+    return monthNames[monthNr];
+}
+
+/**
+ * Calculates the average order value per month/week
+ */
+export class AverageOrderValueMetric implements MetricCalculation {
+    readonly type = MetricType.AverageOrderValue;
+
+    getTitle(ctx: RequestContext): string {
+        return 'average-order-value';
+    }
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry {
+        const label = data.date.toISOString();
+        if (!data.orders.length) {
+            return {
+                label,
+                value: 0,
+            };
+        }
+        const total = data.orders.map(o => o.totalWithTax).reduce((_total, current) => _total + current);
+        const average = Math.round(total / data.orders.length);
+        return {
+            label,
+            value: average,
+        };
+    }
+}
+
+/**
+ * Calculates number of orders
+ */
+export class OrderCountMetric implements MetricCalculation {
+    readonly type = MetricType.OrderCount;
+
+    getTitle(ctx: RequestContext): string {
+        return 'order-count';
+    }
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry {
+        const label = data.date.toISOString();
+        return {
+            label,
+            value: data.orders.length,
+        };
+    }
+}
+/**
+ * Calculates order total
+ */
+export class OrderTotalMetric implements MetricCalculation {
+    readonly type = MetricType.OrderTotal;
+
+    getTitle(ctx: RequestContext): string {
+        return 'order-totals';
+    }
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry {
+        const label = data.date.toISOString();
+        return {
+            label,
+            value: data.orders.map(o => o.totalWithTax).reduce((_total, current) => _total + current, 0),
+        };
+    }
+}

+ 8 - 1
packages/admin-ui-plugin/src/plugin.ts

@@ -19,6 +19,8 @@ import express from 'express';
 import fs from 'fs-extra';
 import path from 'path';
 
+import { adminApiExtensions } from './api/api-extensions';
+import { MetricsResolver } from './api/metrics.resolver';
 import {
     defaultAvailableLanguages,
     defaultLanguage,
@@ -26,6 +28,7 @@ import {
     DEFAULT_APP_PATH,
     loggerCtx,
 } from './constants';
+import { MetricsService } from './service/metrics.service';
 
 /**
  * @description
@@ -102,7 +105,11 @@ export interface AdminUiPluginOptions {
  */
 @VendurePlugin({
     imports: [PluginCommonModule],
-    providers: [],
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [MetricsResolver],
+    },
+    providers: [MetricsService],
     compatibility: '^2.0.0-beta.0',
 })
 export class AdminUiPlugin implements NestModule {

+ 173 - 0
packages/admin-ui-plugin/src/service/metrics.service.ts

@@ -0,0 +1,173 @@
+import { Injectable } from '@nestjs/common';
+import { assertNever } from '@vendure/common/lib/shared-utils';
+import {
+    ConfigService,
+    Logger,
+    Order,
+    RequestContext,
+    TransactionalConnection,
+    TtlCache,
+} from '@vendure/core';
+import {
+    Duration,
+    endOfDay,
+    getDayOfYear,
+    getISOWeek,
+    getMonth,
+    setDayOfYear,
+    startOfDay,
+    sub,
+} from 'date-fns';
+
+import {
+    AverageOrderValueMetric,
+    MetricCalculation,
+    OrderCountMetric,
+    OrderTotalMetric,
+} from '../config/metrics-strategies';
+import { loggerCtx } from '../constants';
+import { MetricInterval, MetricSummary, MetricSummaryEntry, MetricSummaryInput } from '../types';
+
+export type MetricData = {
+    date: Date;
+    orders: Order[];
+};
+
+@Injectable()
+export class MetricsService {
+    private cache = new TtlCache<string, MetricSummary[]>({ ttl: 1000 * 60 * 60 * 24 });
+    metricCalculations: MetricCalculation[];
+    constructor(private connection: TransactionalConnection, private configService: ConfigService) {
+        this.metricCalculations = [
+            new AverageOrderValueMetric(),
+            new OrderCountMetric(),
+            new OrderTotalMetric(),
+        ];
+    }
+
+    async getMetrics(
+        ctx: RequestContext,
+        { interval, types, refresh }: MetricSummaryInput,
+    ): Promise<MetricSummary[]> {
+        // Set 23:59:59.999 as endDate
+        const endDate = endOfDay(new Date());
+        // Check if we have cached result
+        const cacheKey = JSON.stringify({
+            endDate,
+            types: types.sort(),
+            interval,
+            channel: ctx.channel.token,
+        });
+        const cachedMetricList = this.cache.get(cacheKey);
+        if (cachedMetricList && refresh !== true) {
+            Logger.verbose(`Returning cached metrics for channel ${ctx.channel.token}`, loggerCtx);
+            return cachedMetricList;
+        }
+        // No cache, calculating new metrics
+        Logger.verbose(
+            `No cache hit, calculating ${interval} metrics until ${endDate.toISOString()} for channel ${
+                ctx.channel.token
+            } for all orders`,
+            loggerCtx,
+        );
+        const data = await this.loadData(ctx, interval, endDate);
+        const metrics: MetricSummary[] = [];
+        for (const type of types) {
+            const metric = this.metricCalculations.find(m => m.type === type);
+            if (!metric) {
+                continue;
+            }
+            // Calculate entry (month or week)
+            const entries: MetricSummaryEntry[] = [];
+            data.forEach(dataPerTick => {
+                entries.push(metric.calculateEntry(ctx, interval, dataPerTick));
+            });
+            // Create metric with calculated entries
+            metrics.push({
+                interval,
+                title: metric.getTitle(ctx),
+                type: metric.type,
+                entries,
+            });
+        }
+        this.cache.set(cacheKey, metrics);
+        return metrics;
+    }
+
+    async loadData(
+        ctx: RequestContext,
+        interval: MetricInterval,
+        endDate: Date,
+    ): Promise<Map<number, MetricData>> {
+        let nrOfEntries: number;
+        let backInTimeAmount: Duration;
+        const orderRepo = this.connection.getRepository(ctx, Order);
+        // What function to use to get the current Tick of a date (i.e. the week or month number)
+        let getTickNrFn: typeof getMonth | typeof getISOWeek;
+        let maxTick: number;
+        switch (interval) {
+            case MetricInterval.Daily: {
+                nrOfEntries = 30;
+                backInTimeAmount = { days: nrOfEntries };
+                getTickNrFn = getDayOfYear;
+                maxTick = 365;
+                break;
+            }
+            default:
+                assertNever(interval);
+        }
+        const startDate = startOfDay(sub(endDate, backInTimeAmount));
+        const startTick = getTickNrFn(startDate);
+        // Get orders in a loop until we have all
+        let skip = 0;
+        const take = 1000;
+        let hasMoreOrders = true;
+        const orders: Order[] = [];
+        while (hasMoreOrders) {
+            const query = orderRepo
+                .createQueryBuilder('order')
+                .leftJoin('order.channels', 'orderChannel')
+                .where('orderChannel.id=:channelId', { channelId: ctx.channelId })
+                .andWhere('order.orderPlacedAt >= :startDate', {
+                    startDate: startDate.toISOString(),
+                })
+                .skip(skip)
+                .take(take);
+            const [items, nrOfOrders] = await query.getManyAndCount();
+            orders.push(...items);
+            Logger.info(
+                `Fetched orders ${skip}-${skip + take} for channel ${
+                    ctx.channel.token
+                } for ${interval} metrics`,
+                loggerCtx,
+            );
+            skip += items.length;
+            if (orders.length >= nrOfOrders) {
+                hasMoreOrders = false;
+            }
+        }
+        Logger.verbose(
+            `Finished fetching all ${orders.length} orders for channel ${ctx.channel.token} for ${interval} metrics`,
+            loggerCtx,
+        );
+        const dataPerInterval = new Map<number, MetricData>();
+        const ticks = [];
+        for (let i = 1; i <= nrOfEntries; i++) {
+            if (startTick + i >= maxTick) {
+                // make sure we dont go over month 12 or week 52
+                ticks.push(startTick + i - maxTick);
+            } else {
+                ticks.push(startTick + i);
+            }
+        }
+        ticks.forEach(tick => {
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            const ordersInCurrentTick = orders.filter(order => getTickNrFn(order.orderPlacedAt!) === tick);
+            dataPerInterval.set(tick, {
+                orders: ordersInCurrentTick,
+                date: setDayOfYear(endDate, tick),
+            });
+        });
+        return dataPerInterval;
+    }
+}

+ 29 - 0
packages/admin-ui-plugin/src/types.ts

@@ -0,0 +1,29 @@
+import { ID } from '@vendure/core';
+
+export type MetricSummary = {
+    interval: MetricInterval;
+    type: MetricType;
+    title: string;
+    entries: MetricSummaryEntry[];
+};
+
+export enum MetricType {
+    OrderCount = 'OrderCount',
+    OrderTotal = 'OrderTotal',
+    AverageOrderValue = 'AverageOrderValue',
+}
+
+export enum MetricInterval {
+    Daily = 'Daily',
+}
+
+export type MetricSummaryEntry = {
+    label: string;
+    value: number;
+};
+
+export interface MetricSummaryInput {
+    interval: MetricInterval;
+    types: MetricType[];
+    refresh?: boolean;
+}

+ 34 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -2398,6 +2398,34 @@ export type ManualPaymentStateError = ErrorResult & {
     message: Scalars['String'];
 };
 
+export enum MetricInterval {
+    Daily = 'Daily',
+}
+
+export type MetricSummary = {
+    entries: Array<MetricSummaryEntry>;
+    interval: MetricInterval;
+    title: Scalars['String'];
+    type: MetricType;
+};
+
+export type MetricSummaryEntry = {
+    label: Scalars['String'];
+    value: Scalars['Float'];
+};
+
+export type MetricSummaryInput = {
+    interval: MetricInterval;
+    refresh?: InputMaybe<Scalars['Boolean']>;
+    types: Array<MetricType>;
+};
+
+export enum MetricType {
+    AverageOrderValue = 'AverageOrderValue',
+    OrderCount = 'OrderCount',
+    OrderTotal = 'OrderTotal',
+}
+
 export type MimeTypeError = ErrorResult & {
     errorCode: ErrorCode;
     fileName: Scalars['String'];
@@ -4547,6 +4575,8 @@ export type Query = {
     jobs: JobList;
     jobsById: Array<Job>;
     me?: Maybe<CurrentUser>;
+    /** Get metrics for the given interval and metric types. */
+    metricSummary: Array<MetricSummary>;
     order?: Maybe<Order>;
     orders: OrderList;
     paymentMethod?: Maybe<PaymentMethod>;
@@ -4684,6 +4714,10 @@ export type QueryJobsByIdArgs = {
     jobIds: Array<Scalars['ID']>;
 };
 
+export type QueryMetricSummaryArgs = {
+    input?: InputMaybe<MetricSummaryInput>;
+};
+
 export type QueryOrderArgs = {
     id: Scalars['ID'];
 };

+ 37 - 0
packages/common/src/generated-types.ts

@@ -2449,6 +2449,36 @@ export type ManualPaymentStateError = ErrorResult & {
   message: Scalars['String'];
 };
 
+export enum MetricInterval {
+  Daily = 'Daily'
+}
+
+export type MetricSummary = {
+  __typename?: 'MetricSummary';
+  entries: Array<MetricSummaryEntry>;
+  interval: MetricInterval;
+  title: Scalars['String'];
+  type: MetricType;
+};
+
+export type MetricSummaryEntry = {
+  __typename?: 'MetricSummaryEntry';
+  label: Scalars['String'];
+  value: Scalars['Float'];
+};
+
+export type MetricSummaryInput = {
+  interval: MetricInterval;
+  refresh?: InputMaybe<Scalars['Boolean']>;
+  types: Array<MetricType>;
+};
+
+export enum MetricType {
+  AverageOrderValue = 'AverageOrderValue',
+  OrderCount = 'OrderCount',
+  OrderTotal = 'OrderTotal'
+}
+
 export type MimeTypeError = ErrorResult & {
   __typename?: 'MimeTypeError';
   errorCode: ErrorCode;
@@ -4788,6 +4818,8 @@ export type Query = {
   jobs: JobList;
   jobsById: Array<Job>;
   me?: Maybe<CurrentUser>;
+  /** Get metrics for the given interval and metric types. */
+  metricSummary: Array<MetricSummary>;
   order?: Maybe<Order>;
   orders: OrderList;
   paymentMethod?: Maybe<PaymentMethod>;
@@ -4948,6 +4980,11 @@ export type QueryJobsByIdArgs = {
 };
 
 
+export type QueryMetricSummaryArgs = {
+  input?: InputMaybe<MetricSummaryInput>;
+};
+
+
 export type QueryOrderArgs = {
   id: Scalars['ID'];
 };

+ 34 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -2398,6 +2398,34 @@ export type ManualPaymentStateError = ErrorResult & {
     message: Scalars['String'];
 };
 
+export enum MetricInterval {
+    Daily = 'Daily',
+}
+
+export type MetricSummary = {
+    entries: Array<MetricSummaryEntry>;
+    interval: MetricInterval;
+    title: Scalars['String'];
+    type: MetricType;
+};
+
+export type MetricSummaryEntry = {
+    label: Scalars['String'];
+    value: Scalars['Float'];
+};
+
+export type MetricSummaryInput = {
+    interval: MetricInterval;
+    refresh?: InputMaybe<Scalars['Boolean']>;
+    types: Array<MetricType>;
+};
+
+export enum MetricType {
+    AverageOrderValue = 'AverageOrderValue',
+    OrderCount = 'OrderCount',
+    OrderTotal = 'OrderTotal',
+}
+
 export type MimeTypeError = ErrorResult & {
     errorCode: ErrorCode;
     fileName: Scalars['String'];
@@ -4547,6 +4575,8 @@ export type Query = {
     jobs: JobList;
     jobsById: Array<Job>;
     me?: Maybe<CurrentUser>;
+    /** Get metrics for the given interval and metric types. */
+    metricSummary: Array<MetricSummary>;
     order?: Maybe<Order>;
     orders: OrderList;
     paymentMethod?: Maybe<PaymentMethod>;
@@ -4684,6 +4714,10 @@ export type QueryJobsByIdArgs = {
     jobIds: Array<Scalars['ID']>;
 };
 
+export type QueryMetricSummaryArgs = {
+    input?: InputMaybe<MetricSummaryInput>;
+};
+
 export type QueryOrderArgs = {
     id: Scalars['ID'];
 };

+ 2 - 1
packages/dev-server/package.json

@@ -30,6 +30,7 @@
         "commander": "^7.1.0",
         "concurrently": "^5.0.0",
         "csv-stringify": "^5.3.3",
-        "progress": "^2.0.3"
+        "progress": "^2.0.3",
+        "dayjs": "^1.11.7"
     }
 }

+ 92 - 0
packages/dev-server/scripts/generate-past-orders.ts

@@ -0,0 +1,92 @@
+import {
+    bootstrapWorker,
+    CustomerService,
+    isGraphQlErrorResult,
+    Logger,
+    OrderService,
+    ProductVariantService,
+    RequestContextService,
+    ShippingMethodService,
+    TransactionalConnection,
+} from '@vendure/core';
+import dayjs from 'dayjs';
+
+import { devConfig } from '../dev-config';
+
+const loggerCtx = 'DataSync script';
+
+generatePastOrders()
+    .then(() => process.exit(0))
+    .catch(() => process.exit(1));
+
+async function generatePastOrders() {
+    const { app } = await bootstrapWorker(devConfig);
+    const requestContextService = app.get(RequestContextService);
+    const orderService = app.get(OrderService);
+    const customerService = app.get(CustomerService);
+    const productVariantService = app.get(ProductVariantService);
+    const shippingMethodService = app.get(ShippingMethodService);
+    const connection = app.get(TransactionalConnection);
+
+    const ctx = await requestContextService.create({
+        apiType: 'shop',
+    });
+    const ctxAdmin = await requestContextService.create({
+        apiType: 'admin',
+    });
+
+    const { items: variants } = await productVariantService.findAll(ctxAdmin, { take: 500 });
+    const { items: customers } = await customerService.findAll(ctxAdmin, { take: 500 }, ['user']);
+
+    const DAYS_TO_COVER = 30;
+    for (let i = DAYS_TO_COVER; i > 0; i--) {
+        const numberOfOrders = Math.floor(Math.random() * 10) + 5;
+        Logger.info(
+            `Generating ${numberOfOrders} orders for ${dayjs().subtract(i, 'day').format('YYYY-MM-DD')}`,
+        );
+        for (let j = 0; j < numberOfOrders; j++) {
+            const customer = getRandomItem(customers);
+            if (!customer.user) {
+                continue;
+            }
+            const order = await orderService.create(ctx, customer.user.id);
+            const result = await orderService.addItemToOrder(
+                ctx,
+                order.id,
+                getRandomItem(variants).id,
+                Math.floor(Math.random() * 3) + 1,
+            );
+            if (isGraphQlErrorResult(result)) {
+                Logger.error(result.message);
+                continue;
+            }
+            const eligibleShippingMethods = await orderService.getEligibleShippingMethods(ctx, order.id);
+            await orderService.setShippingMethod(ctx, order.id, [getRandomItem(eligibleShippingMethods).id]);
+            const transitionResult = await orderService.transitionToState(ctx, order.id, 'ArrangingPayment');
+            if (isGraphQlErrorResult(transitionResult)) {
+                Logger.error(transitionResult.message);
+                continue;
+            }
+
+            const eligiblePaymentMethods = await orderService.getEligiblePaymentMethods(ctx, order.id);
+            const paymentResult = await orderService.addPaymentToOrder(ctx, order.id, {
+                method: getRandomItem(eligiblePaymentMethods).code,
+                metadata: {},
+            });
+            if (isGraphQlErrorResult(paymentResult)) {
+                Logger.error(paymentResult.message);
+                continue;
+            }
+            const randomHourOfDay = Math.floor(Math.random() * 24);
+            const placedAt = dayjs().subtract(i, 'day').startOf('day').add(randomHourOfDay, 'hour').toDate();
+            await connection.getRepository(ctx, 'Order').update(order.id, {
+                orderPlacedAt: placedAt,
+            });
+        }
+    }
+}
+
+// get random item from array
+function getRandomItem<T>(array: T[]): T {
+    return array[Math.floor(Math.random() * array.length)];
+}

+ 34 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -2398,6 +2398,34 @@ export type ManualPaymentStateError = ErrorResult & {
     message: Scalars['String'];
 };
 
+export enum MetricInterval {
+    Daily = 'Daily',
+}
+
+export type MetricSummary = {
+    entries: Array<MetricSummaryEntry>;
+    interval: MetricInterval;
+    title: Scalars['String'];
+    type: MetricType;
+};
+
+export type MetricSummaryEntry = {
+    label: Scalars['String'];
+    value: Scalars['Float'];
+};
+
+export type MetricSummaryInput = {
+    interval: MetricInterval;
+    refresh?: InputMaybe<Scalars['Boolean']>;
+    types: Array<MetricType>;
+};
+
+export enum MetricType {
+    AverageOrderValue = 'AverageOrderValue',
+    OrderCount = 'OrderCount',
+    OrderTotal = 'OrderTotal',
+}
+
 export type MimeTypeError = ErrorResult & {
     errorCode: ErrorCode;
     fileName: Scalars['String'];
@@ -4547,6 +4575,8 @@ export type Query = {
     jobs: JobList;
     jobsById: Array<Job>;
     me?: Maybe<CurrentUser>;
+    /** Get metrics for the given interval and metric types. */
+    metricSummary: Array<MetricSummary>;
     order?: Maybe<Order>;
     orders: OrderList;
     paymentMethod?: Maybe<PaymentMethod>;
@@ -4684,6 +4714,10 @@ export type QueryJobsByIdArgs = {
     jobIds: Array<Scalars['ID']>;
 };
 
+export type QueryMetricSummaryArgs = {
+    input?: InputMaybe<MetricSummaryInput>;
+};
+
 export type QueryOrderArgs = {
     id: Scalars['ID'];
 };

+ 34 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -2398,6 +2398,34 @@ export type ManualPaymentStateError = ErrorResult & {
     message: Scalars['String'];
 };
 
+export enum MetricInterval {
+    Daily = 'Daily',
+}
+
+export type MetricSummary = {
+    entries: Array<MetricSummaryEntry>;
+    interval: MetricInterval;
+    title: Scalars['String'];
+    type: MetricType;
+};
+
+export type MetricSummaryEntry = {
+    label: Scalars['String'];
+    value: Scalars['Float'];
+};
+
+export type MetricSummaryInput = {
+    interval: MetricInterval;
+    refresh?: InputMaybe<Scalars['Boolean']>;
+    types: Array<MetricType>;
+};
+
+export enum MetricType {
+    AverageOrderValue = 'AverageOrderValue',
+    OrderCount = 'OrderCount',
+    OrderTotal = 'OrderTotal',
+}
+
 export type MimeTypeError = ErrorResult & {
     errorCode: ErrorCode;
     fileName: Scalars['String'];
@@ -4547,6 +4575,8 @@ export type Query = {
     jobs: JobList;
     jobsById: Array<Job>;
     me?: Maybe<CurrentUser>;
+    /** Get metrics for the given interval and metric types. */
+    metricSummary: Array<MetricSummary>;
     order?: Maybe<Order>;
     orders: OrderList;
     paymentMethod?: Maybe<PaymentMethod>;
@@ -4684,6 +4714,10 @@ export type QueryJobsByIdArgs = {
     jobIds: Array<Scalars['ID']>;
 };
 
+export type QueryMetricSummaryArgs = {
+    input?: InputMaybe<MetricSummaryInput>;
+};
+
 export type QueryOrderArgs = {
     id: Scalars['ID'];
 };

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


+ 21 - 1
yarn.lock

@@ -2382,6 +2382,13 @@
   dependencies:
     regenerator-runtime "^0.13.11"
 
+"@babel/runtime@^7.21.0":
+  version "7.22.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
+  integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
+  dependencies:
+    regenerator-runtime "^0.13.11"
+
 "@babel/template@7.20.7", "@babel/template@^7.18.10", "@babel/template@^7.20.7":
   version "7.20.7"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
@@ -5587,6 +5594,7 @@
     core-js "^3.29.0"
     dayjs "^1.10.4"
     graphql "16.6.0"
+    just-extend "^6.2.0"
     messageformat "2.3.0"
     ngx-pagination "^6.0.3"
     ngx-translate-messageformat-compiler "^6.2.0"
@@ -7329,6 +7337,11 @@ chardet@^0.7.0:
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
+chartist@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/chartist/-/chartist-1.3.0.tgz#ade0cadeb3d443377438a17b6866ccd125d5f079"
+  integrity sha512-M3ckI3ua7EHt08WLZvdi3QXn5g+in27qU6TxjI5bpriS6QwIZlWtisyUhFbpGclW546SlT3SL9oq0vFFDiAo6g==
+
 check-disk-space@3.3.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.3.1.tgz#10c4c8706fdd16d3e5c3572a16aa95efd0b4d40b"
@@ -8346,6 +8359,13 @@ date-fns@^2.0.1, date-fns@^2.16.1, date-fns@^2.28.0:
   resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
   integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
 
+date-fns@^2.30.0:
+  version "2.30.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+  integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+  dependencies:
+    "@babel/runtime" "^7.21.0"
+
 date-format@^4.0.14:
   version "4.0.14"
   resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400"
@@ -8361,7 +8381,7 @@ dateformat@^3.0.0, dateformat@^3.0.3:
   resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
   integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
 
-dayjs@^1.10.4:
+dayjs@^1.10.4, dayjs@^1.11.7:
   version "1.11.7"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
   integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==

Some files were not shown because too many files changed in this diff