feat(dashboard): add support for additional metrics in overview (#3052)

### **User description**
resolves https://github.com/nhost/nhost/issues/3017


___

### **PR Type**
Enhancement, Other


___

### **Description**
- Introduced new metrics in the dashboard overview, including monthly
and daily active users, total users, and storage.
- Implemented a new GraphQL query `GetUserProjectMetrics` to fetch
user-related metrics.
- Updated the `OverviewMetrics` component to display the newly added
metrics.
- Enhanced the GraphQL schema with new fields and queries to support the
additional metrics.
- Added a changeset to document the new feature in the dashboard.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>OverviewMetrics.tsx</strong><dd><code>Add support for
additional user and storage metrics in overview</code></dd></summary>
<hr>


dashboard/src/features/orgs/projects/overview/components/OverviewMetrics/OverviewMetrics.tsx

<li>Added new metrics for monthly and daily active users, total users,
and <br>storage.<br> <li> Integrated
<code>useGetUserProjectMetricsQuery</code> for fetching user-related
<br>metrics.<br> <li> Updated metrics card elements to display new
metrics.<br> <li> Removed redundant data checks and improved error
handling.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3052/files#diff-de881837e53f594075bb725282b02e92c2cb281f8f6a438fdbaa2e3254907fd1">+90/-17</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>graphql.ts</strong><dd><code>Update GraphQL types and
queries for user metrics</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/utils/__generated__/graphql.ts

<li>Added new GraphQL query types for user project metrics.<br> <li>
Introduced <code>automaticDeploys</code> field in the <code>Apps</code>
type.<br> <li> Updated generated types to include new fields and
queries.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3052/files#diff-fbd5db84b560b1c91675004448c6c7fa0dcbfb28b9eb05d53b03e6cb7b83ebac">+87/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>getUserProjectMetrics.gql</strong><dd><code>Add GraphQL
query for user project metrics</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/gql/organizations/getUserProjectMetrics.gql

- Created new GraphQL query for fetching user project metrics.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3052/files#diff-902523302d8b32d218ef665a252dec5b9cbcf5fbab0cbb32845c441b01eaa28e">+28/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>khaki-pets-argue.md</strong><dd><code>Add changeset for
dashboard metrics feature</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/khaki-pets-argue.md

- Added changeset for new feature in dashboard.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3052/files#diff-51eb36ca77dae90a865c185e0d528fcaa4b836f3a0469550513f68003bf11c9a">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**: Comment `/help "your question"` on any pull
request to receive relevant information

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
This commit is contained in:
Hassan Ben Jobrane
2024-12-05 16:14:15 +01:00
committed by GitHub
parent 1b5dc5e7f5
commit 86ecf27b23
5 changed files with 273 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
---
'@nhost/dashboard': minor
---
feat: add support for additional metrics in overview

View File

@@ -1,54 +1,132 @@
import { Text } from '@/components/ui/v2/Text';
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import type { MetricsCardProps } from '@/features/orgs/projects/overview/components/MetricsCard';
import { MetricsCard } from '@/features/orgs/projects/overview/components/MetricsCard';
import { prettifyNumber } from '@/utils/prettifyNumber';
import { prettifySize } from '@/utils/prettifySize';
import { useGetProjectMetricsQuery } from '@/utils/__generated__/graphql';
import {
useGetProjectMetricsQuery,
useGetProjectRequestsMetricQuery,
useGetUserProjectMetricsQuery,
} from '@/utils/__generated__/graphql';
import { twMerge } from 'tailwind-merge';
import { prettifySize } from '@/utils/prettifySize';
import { formatISO, startOfDay, startOfMonth, subMinutes } from 'date-fns';
const now = new Date();
export default function OverviewMetrics() {
const { project } = useProject();
const { data, loading, error } = useGetProjectMetricsQuery({
const remoteProjectGQLClient = useRemoteApplicationGQLClient();
const {
data: {
allUsers: { aggregate: { count: allUsers = 0 } = {} } = {},
dailyActiveUsers: {
aggregate: { count: dailyActiveUsers = 0 } = {},
} = {},
monthlyActiveUsers: {
aggregate: { count: monthlyActiveUsers = 0 } = {},
} = {},
filesAggregate: {
aggregate: { sum: { size: totalStorage = 0 } = {} } = {},
} = {},
} = {},
} = useGetUserProjectMetricsQuery({
client: remoteProjectGQLClient,
variables: {
appId: project?.id,
startOfMonth: startOfMonth(new Date()),
today: startOfDay(new Date()),
},
skip: !project,
});
const {
data: {
totalRequests: { value: totalRequestsInLastFiveMinutes = 0 } = {},
} = {},
} = useGetProjectRequestsMetricQuery({
variables: {
appId: project.id,
from: formatISO(subMinutes(new Date(), 6)), // 6 mns earlier
to: formatISO(subMinutes(new Date(), 1)), // 1 mn earlier
},
skip: !project,
pollInterval: 1000 * 60 * 5, // Poll every 5 minutes
});
const {
data: {
functionsDuration: { value: functionsDuration = 0 } = {},
totalRequests: { value: totalRequests = 0 } = {},
postgresVolumeUsage: { value: postgresVolumeUsage = 0 } = {},
egressVolume: { value: egressVolume = 0 } = {},
} = {},
loading,
error,
} = useGetProjectMetricsQuery({
variables: {
appId: project.id,
subdomain: project?.subdomain,
from: new Date(now.getFullYear(), now.getMonth(), 1),
},
skip: !project?.id,
skip: !project,
});
const cardElements: MetricsCardProps[] = [
{
label: 'CPU Usage Seconds',
tooltip: 'Total time the service has used the CPUs',
value: prettifyNumber(data?.cpuSecondsUsage?.value || 0),
label: 'Daily Active Users',
tooltip: 'Unique users active today',
value: prettifyNumber(dailyActiveUsers),
},
{
label: 'Monthly Active Users',
tooltip: 'Unique users active this month',
value: prettifyNumber(monthlyActiveUsers),
},
{
label: 'All Users',
tooltip: 'Total registered users',
value: prettifyNumber(allUsers),
},
{
label: 'RPS',
tooltip: 'Requests Per Second (RPS) measured in the last 5 minutes',
value: prettifyNumber(totalRequestsInLastFiveMinutes / 300, {
numberOfDecimals: 2,
}),
},
{
label: 'Total Requests',
tooltip:
'Total amount of requests your services have received excluding functions',
value: prettifyNumber(data?.totalRequests?.value || 0, {
numberOfDecimals: data?.totalRequests?.value > 1000 ? 2 : 0,
tooltip: 'Total service requests this month so far (excluding functions)',
value: prettifyNumber(totalRequests || 0, {
numberOfDecimals: totalRequests > 1000 ? 2 : 0,
}),
},
{
label: 'Function Invocations',
tooltip: 'Number of times your functions have been called',
value: prettifyNumber(data?.functionInvocations?.value || 0, {
numberOfDecimals: 0,
}),
label: 'Egress',
tooltip: 'Total outgoing data transfer this month so far',
value: prettifySize(egressVolume),
},
{
label: 'Logs',
tooltip: 'Amount of logs stored',
value: prettifySize(data?.logsVolume?.value || 0),
label: 'Functions Duration',
tooltip: 'Total Functions execution this month so far',
value: prettifyNumber(functionsDuration),
},
{
label: 'Storage',
tooltip: 'Total size of stored files in the storage service',
value: prettifySize(totalStorage || 0),
},
{
label: 'Postgres Volume Usage',
tooltip: 'Used storage in the Postgres database',
value: prettifySize(postgresVolumeUsage),
},
];
if (!data && error) {
if (error) {
throw error;
}

View File

@@ -0,0 +1,9 @@
query GetProjectRequestsMetric(
$appId: String!
$from: Timestamp
$to: Timestamp
) {
totalRequests: getTotalRequests(appID: $appId, from: $from, to: $to) {
value
}
}

View File

@@ -0,0 +1,28 @@
query GetUserProjectMetrics($startOfMonth: timestamptz!, $today: timestamptz!) {
monthlyActiveUsers: usersAggregate(
where: { lastSeen: { _gte: $startOfMonth, _lte: $today } }
) {
aggregate {
count
}
}
dailyActiveUsers: usersAggregate(where: { lastSeen: { _gte: $today } }) {
aggregate {
count
}
}
allUsers: usersAggregate {
aggregate {
count
}
}
filesAggregate {
aggregate {
count
sum {
size
}
}
}
}

View File

@@ -4341,6 +4341,7 @@ export type Apps = {
appStates: Array<AppStateHistory>;
/** An aggregate relationship */
appStates_aggregate: AppStateHistory_Aggregate;
automaticDeploys: Scalars['Boolean'];
/** An array relationship */
backups: Array<Backups>;
/** An aggregate relationship */
@@ -4619,6 +4620,7 @@ export type Apps_Bool_Exp = {
_or?: InputMaybe<Array<Apps_Bool_Exp>>;
appStates?: InputMaybe<AppStateHistory_Bool_Exp>;
appStates_aggregate?: InputMaybe<AppStateHistory_Aggregate_Bool_Exp>;
automaticDeploys?: InputMaybe<Boolean_Comparison_Exp>;
backups?: InputMaybe<Backups_Bool_Exp>;
backups_aggregate?: InputMaybe<Backups_Aggregate_Bool_Exp>;
billingDedicatedCompute?: InputMaybe<Billing_Dedicated_Compute_Bool_Exp>;
@@ -4696,6 +4698,7 @@ export type Apps_Inc_Input = {
/** input type for inserting data into table "apps" */
export type Apps_Insert_Input = {
appStates?: InputMaybe<AppStateHistory_Arr_Rel_Insert_Input>;
automaticDeploys?: InputMaybe<Scalars['Boolean']>;
backups?: InputMaybe<Backups_Arr_Rel_Insert_Input>;
billingDedicatedCompute?: InputMaybe<Billing_Dedicated_Compute_Obj_Rel_Insert_Input>;
billingSubscriptions?: InputMaybe<Billing_Subscriptions_Obj_Rel_Insert_Input>;
@@ -4862,6 +4865,7 @@ export type Apps_On_Conflict = {
/** Ordering options when selecting data from "apps". */
export type Apps_Order_By = {
appStates_aggregate?: InputMaybe<AppStateHistory_Aggregate_Order_By>;
automaticDeploys?: InputMaybe<Order_By>;
backups_aggregate?: InputMaybe<Backups_Aggregate_Order_By>;
billingDedicatedCompute?: InputMaybe<Billing_Dedicated_Compute_Order_By>;
billingSubscriptions?: InputMaybe<Billing_Subscriptions_Order_By>;
@@ -4913,6 +4917,8 @@ export type Apps_Prepend_Input = {
/** select columns of table "apps" */
export enum Apps_Select_Column {
/** column name */
AutomaticDeploys = 'automaticDeploys',
/** column name */
CreatedAt = 'createdAt',
/** column name */
@@ -4965,6 +4971,8 @@ export enum Apps_Select_Column {
/** select "apps_aggregate_bool_exp_bool_and_arguments_columns" columns of table "apps" */
export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_And_Arguments_Columns {
/** column name */
AutomaticDeploys = 'automaticDeploys',
/** column name */
IsLocked = 'isLocked',
/** column name */
@@ -4973,6 +4981,8 @@ export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_And_Arguments_Column
/** select "apps_aggregate_bool_exp_bool_or_arguments_columns" columns of table "apps" */
export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_Or_Arguments_Columns {
/** column name */
AutomaticDeploys = 'automaticDeploys',
/** column name */
IsLocked = 'isLocked',
/** column name */
@@ -4981,6 +4991,7 @@ export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_Or_Arguments_Columns
/** input type for updating data in table "apps" */
export type Apps_Set_Input = {
automaticDeploys?: InputMaybe<Scalars['Boolean']>;
createdAt?: InputMaybe<Scalars['timestamptz']>;
creatorUserId?: InputMaybe<Scalars['uuid']>;
currentState?: InputMaybe<Scalars['Int']>;
@@ -5055,6 +5066,7 @@ export type Apps_Stream_Cursor_Input = {
/** Initial value of the column from where the streaming should start */
export type Apps_Stream_Cursor_Value_Input = {
automaticDeploys?: InputMaybe<Scalars['Boolean']>;
createdAt?: InputMaybe<Scalars['timestamptz']>;
creatorUserId?: InputMaybe<Scalars['uuid']>;
currentState?: InputMaybe<Scalars['Int']>;
@@ -5096,6 +5108,8 @@ export type Apps_Sum_Order_By = {
/** update columns of table "apps" */
export enum Apps_Update_Column {
/** column name */
AutomaticDeploys = 'automaticDeploys',
/** column name */
CreatedAt = 'createdAt',
/** column name */
@@ -27411,6 +27425,15 @@ export type GetProjectMetricsQueryVariables = Exact<{
export type GetProjectMetricsQuery = { __typename?: 'query_root', logsVolume: { __typename?: 'Metrics', value: any }, cpuSecondsUsage: { __typename?: 'Metrics', value: any }, functionInvocations: { __typename?: 'Metrics', value: any }, functionsDuration: { __typename?: 'Metrics', value: any }, postgresVolumeCapacity: { __typename?: 'Metrics', value: any }, postgresVolumeUsage: { __typename?: 'Metrics', value: any }, totalRequests: { __typename?: 'Metrics', value: any }, egressVolume: { __typename?: 'Metrics', value: any } };
export type GetProjectRequestsMetricQueryVariables = Exact<{
appId: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
}>;
export type GetProjectRequestsMetricQuery = { __typename?: 'query_root', totalRequests: { __typename?: 'Metrics', value: any } };
export type GetProjectServicesHealthQueryVariables = Exact<{
appId: Scalars['String'];
}>;
@@ -27853,6 +27876,14 @@ export type GetProjectsQueryVariables = Exact<{
export type GetProjectsQuery = { __typename?: 'query_root', apps: Array<{ __typename?: 'apps', id: any, name: string, slug: string, createdAt: any, subdomain: string, region: { __typename?: 'regions', id: any, name: string }, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }> }> };
export type GetUserProjectMetricsQueryVariables = Exact<{
startOfMonth: Scalars['timestamptz'];
today: Scalars['timestamptz'];
}>;
export type GetUserProjectMetricsQuery = { __typename?: 'query_root', monthlyActiveUsers: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, dailyActiveUsers: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, allUsers: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, filesAggregate: { __typename?: 'files_aggregate', aggregate?: { __typename?: 'files_aggregate_fields', count: number, sum?: { __typename?: 'files_sum_fields', size?: number | null } | null } | null } };
export type InsertOrgApplicationMutationVariables = Exact<{
app: Apps_Insert_Input;
}>;
@@ -29936,6 +29967,46 @@ export type GetProjectMetricsQueryResult = Apollo.QueryResult<GetProjectMetricsQ
export function refetchGetProjectMetricsQuery(variables: GetProjectMetricsQueryVariables) {
return { query: GetProjectMetricsDocument, variables: variables }
}
export const GetProjectRequestsMetricDocument = gql`
query GetProjectRequestsMetric($appId: String!, $from: Timestamp, $to: Timestamp) {
totalRequests: getTotalRequests(appID: $appId, from: $from, to: $to) {
value
}
}
`;
/**
* __useGetProjectRequestsMetricQuery__
*
* To run a query within a React component, call `useGetProjectRequestsMetricQuery` and pass it any options that fit your needs.
* When your component renders, `useGetProjectRequestsMetricQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetProjectRequestsMetricQuery({
* variables: {
* appId: // value for 'appId'
* from: // value for 'from'
* to: // value for 'to'
* },
* });
*/
export function useGetProjectRequestsMetricQuery(baseOptions: Apollo.QueryHookOptions<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>(GetProjectRequestsMetricDocument, options);
}
export function useGetProjectRequestsMetricLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>(GetProjectRequestsMetricDocument, options);
}
export type GetProjectRequestsMetricQueryHookResult = ReturnType<typeof useGetProjectRequestsMetricQuery>;
export type GetProjectRequestsMetricLazyQueryHookResult = ReturnType<typeof useGetProjectRequestsMetricLazyQuery>;
export type GetProjectRequestsMetricQueryResult = Apollo.QueryResult<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>;
export function refetchGetProjectRequestsMetricQuery(variables: GetProjectRequestsMetricQueryVariables) {
return { query: GetProjectRequestsMetricDocument, variables: variables }
}
export const GetProjectServicesHealthDocument = gql`
query getProjectServicesHealth($appId: String!) {
getProjectStatus(appID: $appId) {
@@ -32534,6 +32605,67 @@ export type GetProjectsQueryResult = Apollo.QueryResult<GetProjectsQuery, GetPro
export function refetchGetProjectsQuery(variables: GetProjectsQueryVariables) {
return { query: GetProjectsDocument, variables: variables }
}
export const GetUserProjectMetricsDocument = gql`
query GetUserProjectMetrics($startOfMonth: timestamptz!, $today: timestamptz!) {
monthlyActiveUsers: usersAggregate(
where: {lastSeen: {_gte: $startOfMonth, _lte: $today}}
) {
aggregate {
count
}
}
dailyActiveUsers: usersAggregate(where: {lastSeen: {_gte: $today}}) {
aggregate {
count
}
}
allUsers: usersAggregate {
aggregate {
count
}
}
filesAggregate {
aggregate {
count
sum {
size
}
}
}
}
`;
/**
* __useGetUserProjectMetricsQuery__
*
* To run a query within a React component, call `useGetUserProjectMetricsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetUserProjectMetricsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetUserProjectMetricsQuery({
* variables: {
* startOfMonth: // value for 'startOfMonth'
* today: // value for 'today'
* },
* });
*/
export function useGetUserProjectMetricsQuery(baseOptions: Apollo.QueryHookOptions<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>(GetUserProjectMetricsDocument, options);
}
export function useGetUserProjectMetricsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>(GetUserProjectMetricsDocument, options);
}
export type GetUserProjectMetricsQueryHookResult = ReturnType<typeof useGetUserProjectMetricsQuery>;
export type GetUserProjectMetricsLazyQueryHookResult = ReturnType<typeof useGetUserProjectMetricsLazyQuery>;
export type GetUserProjectMetricsQueryResult = Apollo.QueryResult<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>;
export function refetchGetUserProjectMetricsQuery(variables: GetUserProjectMetricsQueryVariables) {
return { query: GetUserProjectMetricsDocument, variables: variables }
}
export const InsertOrgApplicationDocument = gql`
mutation insertOrgApplication($app: apps_insert_input!) {
insertApp(object: $app) {