Skip to content

Commit c6cf719

Browse files
authored
feat: show user limit on active users chart (#10101)
1 parent 38bb854 commit c6cf719

File tree

9 files changed

+118
-7
lines changed

9 files changed

+118
-7
lines changed

site/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"canvas": "2.11.0",
5555
"chart.js": "4.4.0",
5656
"chartjs-adapter-date-fns": "3.0.0",
57+
"chartjs-plugin-annotation": "3.0.1",
5758
"chroma-js": "2.4.2",
5859
"color-convert": "2.0.1",
5960
"cron-parser": "4.9.0",

site/pnpm-lock.yaml

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/components/ActiveUserChart/ActiveUserChart.tsx

+36
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import dayjs from "dayjs";
2525
import { FC } from "react";
2626
import { Line } from "react-chartjs-2";
27+
import annotationPlugin from "chartjs-plugin-annotation";
2728

2829
ChartJS.register(
2930
CategoryScale,
@@ -35,16 +36,21 @@ ChartJS.register(
3536
Title,
3637
Tooltip,
3738
Legend,
39+
annotationPlugin,
3840
);
3941

42+
const USER_LIMIT_DISPLAY_THRESHOLD = 60;
43+
4044
export interface ActiveUserChartProps {
4145
data: { date: string; amount: number }[];
4246
interval: "day" | "week";
47+
userLimit: number | undefined;
4348
}
4449

4550
export const ActiveUserChart: FC<ActiveUserChartProps> = ({
4651
data,
4752
interval,
53+
userLimit,
4854
}) => {
4955
const theme: Theme = useTheme();
5056

@@ -57,6 +63,24 @@ export const ActiveUserChart: FC<ActiveUserChartProps> = ({
5763
const options: ChartOptions<"line"> = {
5864
responsive: true,
5965
plugins: {
66+
annotation: {
67+
annotations: [
68+
{
69+
type: "line",
70+
scaleID: "y",
71+
display: shouldDisplayUserLimit(userLimit, chartData),
72+
value: userLimit,
73+
borderColor: theme.palette.secondary.contrastText,
74+
borderWidth: 5,
75+
label: {
76+
content: "User limit",
77+
color: theme.palette.primary.contrastText,
78+
display: true,
79+
font: { weight: "normal" },
80+
},
81+
},
82+
],
83+
},
6084
legend: {
6185
display: false,
6286
},
@@ -127,3 +151,15 @@ export const ActiveUsersTitle = () => {
127151
</Box>
128152
);
129153
};
154+
155+
function shouldDisplayUserLimit(
156+
userLimit: number | undefined,
157+
activeUsers: number[],
158+
): boolean {
159+
if (!userLimit || activeUsers.length === 0) {
160+
return false;
161+
}
162+
return (
163+
Math.max(...activeUsers) >= (userLimit * USER_LIMIT_DISPLAY_THRESHOLD) / 100
164+
);
165+
}

site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { pageTitle } from "utils/page";
55
import { GeneralSettingsPageView } from "./GeneralSettingsPageView";
66
import { useQuery } from "react-query";
77
import { deploymentDAUs } from "api/queries/deployment";
8+
import { entitlements } from "api/queries/entitlements";
89

910
const GeneralSettingsPage: FC = () => {
1011
const { deploymentValues } = useDeploySettings();
1112
const deploymentDAUsQuery = useQuery(deploymentDAUs());
13+
const entitlementsQuery = useQuery(entitlements());
1214

1315
return (
1416
<>
@@ -19,6 +21,7 @@ const GeneralSettingsPage: FC = () => {
1921
deploymentOptions={deploymentValues.options}
2022
deploymentDAUs={deploymentDAUsQuery.data}
2123
deploymentDAUsError={deploymentDAUsQuery.error}
24+
entitlements={entitlementsQuery.data}
2225
/>
2326
</>
2427
);

site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { Meta, StoryObj } from "@storybook/react";
2-
import { mockApiError, MockDeploymentDAUResponse } from "testHelpers/entities";
2+
import {
3+
mockApiError,
4+
MockDeploymentDAUResponse,
5+
MockEntitlementsWithUserLimit,
6+
} from "testHelpers/entities";
37
import { GeneralSettingsPageView } from "./GeneralSettingsPageView";
48

59
const meta: Meta<typeof GeneralSettingsPageView> = {
@@ -44,6 +48,13 @@ type Story = StoryObj<typeof GeneralSettingsPageView>;
4448

4549
export const Page: Story = {};
4650

51+
export const WithUserLimit: Story = {
52+
args: {
53+
deploymentDAUs: MockDeploymentDAUResponse,
54+
entitlements: MockEntitlementsWithUserLimit,
55+
},
56+
};
57+
4758
export const NoDAUs: Story = {
4859
args: {
4960
deploymentDAUs: undefined,

site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Box from "@mui/material/Box";
2-
import { ClibaseOption, DAUsResponse } from "api/typesGenerated";
2+
import { ClibaseOption, DAUsResponse, Entitlements } from "api/typesGenerated";
33
import { ErrorAlert } from "components/Alert/ErrorAlert";
44
import {
55
ActiveUserChart,
@@ -16,11 +16,13 @@ export type GeneralSettingsPageViewProps = {
1616
deploymentOptions: ClibaseOption[];
1717
deploymentDAUs?: DAUsResponse;
1818
deploymentDAUsError: unknown;
19+
entitlements: Entitlements | undefined;
1920
};
2021
export const GeneralSettingsPageView = ({
2122
deploymentOptions,
2223
deploymentDAUs,
2324
deploymentDAUsError,
25+
entitlements,
2426
}: GeneralSettingsPageViewProps): JSX.Element => {
2527
return (
2628
<>
@@ -36,7 +38,15 @@ export const GeneralSettingsPageView = ({
3638
{deploymentDAUs && (
3739
<Box height={200} sx={{ mb: 3 }}>
3840
<ChartSection title={<ActiveUsersTitle />}>
39-
<ActiveUserChart data={deploymentDAUs.entries} interval="day" />
41+
<ActiveUserChart
42+
data={deploymentDAUs.entries}
43+
interval="day"
44+
userLimit={
45+
entitlements?.features.user_limit.enabled
46+
? entitlements?.features.user_limit.limit
47+
: undefined
48+
}
49+
/>
4050
</ChartSection>
4151
</Box>
4252
)}

site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { TemplateInsightsPageView } from "./TemplateInsightsPage";
3+
import { MockEntitlementsWithUserLimit } from "testHelpers/entities";
34

45
const meta: Meta<typeof TemplateInsightsPageView> = {
56
title: "pages/TemplateInsightsPageView",
@@ -515,7 +516,7 @@ export const Loaded: Story = {
515516
end_time: "2023-07-25T00:00:00Z",
516517
template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"],
517518
interval: "day",
518-
active_users: 11,
519+
active_users: 16,
519520
},
520521
],
521522
},
@@ -861,3 +862,11 @@ export const Loaded: Story = {
861862
},
862863
},
863864
};
865+
866+
export const LoadedWithUserLimit: Story = {
867+
...Loaded,
868+
args: {
869+
...Loaded.args,
870+
entitlements: MockEntitlementsWithUserLimit,
871+
},
872+
};

site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Helmet } from "react-helmet-async";
2121
import { getTemplatePageTitle } from "../utils";
2222
import { Loader } from "components/Loader/Loader";
2323
import {
24+
Entitlements,
2425
Template,
2526
TemplateAppUsage,
2627
TemplateInsightsResponse,
@@ -48,6 +49,7 @@ import {
4849
insightsUserLatency,
4950
} from "api/queries/insights";
5051
import { useSearchParams } from "react-router-dom";
52+
import { entitlements } from "api/queries/entitlements";
5153

5254
const DEFAULT_NUMBER_OF_WEEKS = numberOfWeeksOptions[0];
5355

@@ -75,6 +77,7 @@ export default function TemplateInsightsPage() {
7577
const { data: templateInsights } = useQuery(insightsTemplate(insightsFilter));
7678
const { data: userLatency } = useQuery(insightsUserLatency(commonFilters));
7779
const { data: userActivity } = useQuery(insightsUserActivity(commonFilters));
80+
const { data: entitlementsQuery } = useQuery(entitlements());
7881

7982
return (
8083
<>
@@ -106,6 +109,7 @@ export default function TemplateInsightsPage() {
106109
userLatency={userLatency}
107110
userActivity={userActivity}
108111
interval={interval}
112+
entitlements={entitlementsQuery}
109113
/>
110114
</>
111115
);
@@ -146,12 +150,14 @@ export const TemplateInsightsPageView = ({
146150
templateInsights,
147151
userLatency,
148152
userActivity,
153+
entitlements,
149154
controls,
150155
interval,
151156
}: {
152157
templateInsights: TemplateInsightsResponse | undefined;
153158
userLatency: UserLatencyInsightsResponse | undefined;
154159
userActivity: UserActivityInsightsResponse | undefined;
160+
entitlements: Entitlements | undefined;
155161
controls: ReactNode;
156162
interval: InsightsInterval;
157163
}) => {
@@ -178,6 +184,11 @@ export const TemplateInsightsPageView = ({
178184
<ActiveUsersPanel
179185
sx={{ gridColumn: "span 2" }}
180186
interval={interval}
187+
userLimit={
188+
entitlements?.features.user_limit.enabled
189+
? entitlements?.features.user_limit.limit
190+
: undefined
191+
}
181192
data={templateInsights?.interval_reports}
182193
/>
183194
<UsersLatencyPanel data={userLatency} />
@@ -198,10 +209,12 @@ export const TemplateInsightsPageView = ({
198209
const ActiveUsersPanel = ({
199210
data,
200211
interval,
212+
userLimit,
201213
...panelProps
202214
}: PanelProps & {
203215
data: TemplateInsightsResponse["interval_reports"] | undefined;
204216
interval: InsightsInterval;
217+
userLimit: number | undefined;
205218
}) => {
206219
return (
207220
<Panel {...panelProps}>
@@ -216,6 +229,7 @@ const ActiveUsersPanel = ({
216229
{data && data.length > 0 && (
217230
<ActiveUserChart
218231
interval={interval}
232+
userLimit={userLimit}
219233
data={data.map((d) => ({
220234
amount: d.active_users,
221235
date: d.start_time,

site/src/testHelpers/entities.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export const MockTemplateDAUResponse: TypesGen.DAUsResponse = {
3030
export const MockDeploymentDAUResponse: TypesGen.DAUsResponse = {
3131
tz_hour_offset: 0,
3232
entries: [
33-
{ date: "2022-08-27T00:00:00Z", amount: 1 },
34-
{ date: "2022-08-29T00:00:00Z", amount: 2 },
35-
{ date: "2022-08-30T00:00:00Z", amount: 1 },
33+
{ date: "2022-08-27T00:00:00Z", amount: 10 },
34+
{ date: "2022-08-29T00:00:00Z", amount: 22 },
35+
{ date: "2022-08-30T00:00:00Z", amount: 14 },
3636
],
3737
};
3838
export const MockSessionToken: TypesGen.LoginWithPasswordResponse = {
@@ -1925,6 +1925,22 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = {
19251925
}),
19261926
};
19271927

1928+
export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = {
1929+
errors: [],
1930+
warnings: [],
1931+
has_license: true,
1932+
require_telemetry: false,
1933+
trial: false,
1934+
refreshed_at: "2022-05-20T16:45:57.122Z",
1935+
features: withDefaultFeatures({
1936+
user_limit: {
1937+
enabled: true,
1938+
entitlement: "entitled",
1939+
limit: 25,
1940+
},
1941+
}),
1942+
};
1943+
19281944
export const MockExperiments: TypesGen.Experiment[] = ["moons"];
19291945

19301946
export const MockAuditLog: TypesGen.AuditLog = {

0 commit comments

Comments
 (0)