From 1f7120fa66960d955be1f4b158ad0a3e46d5b441 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 17:03:39 +0000 Subject: [PATCH 01/24] Add insights page --- site/src/AppRouter.tsx | 5 +++++ site/src/components/TemplateLayout/TemplateLayout.tsx | 11 +++++++++++ .../TemplateInsightsPage/TemplateInsightsPage.tsx | 5 +++++ 3 files changed, 21 insertions(+) create mode 100644 site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 0b31a887371c3..34cbc0ed6d246 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -179,6 +179,10 @@ const AddNewLicensePage = lazy( const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), ) +const TemplateInsightsPage = lazy( + () => + import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"), +) export const AppRouter: FC = () => { return ( @@ -212,6 +216,7 @@ export const AppRouter: FC = () => { } /> } /> } /> + } /> } /> diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index 9247c21d77ded..21a6c68764f10 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -157,6 +157,17 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ > Embed + + combineClasses([ + styles.tabItem, + isActive ? styles.tabItemActive : undefined, + ]) + } + > + Insights + diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx new file mode 100644 index 0000000000000..d9b2f7808a1e4 --- /dev/null +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -0,0 +1,5 @@ +const TemplateInsightsPage = () => { + return
TemplateInsightsPage
+} + +export default TemplateInsightsPage From ed5784e8b906cadc41c2f4db8afc34c4a7429dac Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 17:05:38 +0000 Subject: [PATCH 02/24] Add experiment --- coderd/apidoc/docs.go | 6 ++++-- coderd/apidoc/swagger.json | 6 ++++-- codersdk/deployment.go | 7 ++++++- docs/api/schemas.md | 1 + site/src/api/typesGenerated.ts | 2 ++ 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8c765a3e9b2b5..58beeeed69e16 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7838,7 +7838,8 @@ const docTemplate = `{ "tailnet_ha_coordinator", "convert-to-oidc", "single_tailnet", - "template_restart_requirement" + "template_restart_requirement", + "template_insights_page" ], "x-enum-varnames": [ "ExperimentMoons", @@ -7846,7 +7847,8 @@ const docTemplate = `{ "ExperimentTailnetHACoordinator", "ExperimentConvertToOIDC", "ExperimentSingleTailnet", - "ExperimentTemplateRestartRequirement" + "ExperimentTemplateRestartRequirement", + "ExperimentTemplateInsightsPage" ] }, "codersdk.Feature": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 2dc5fedea55ea..51e30be4b51dc 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7015,7 +7015,8 @@ "tailnet_ha_coordinator", "convert-to-oidc", "single_tailnet", - "template_restart_requirement" + "template_restart_requirement", + "template_insights_page" ], "x-enum-varnames": [ "ExperimentMoons", @@ -7023,7 +7024,8 @@ "ExperimentTailnetHACoordinator", "ExperimentConvertToOIDC", "ExperimentSingleTailnet", - "ExperimentTemplateRestartRequirement" + "ExperimentTemplateRestartRequirement", + "ExperimentTemplateInsightsPage" ] }, "codersdk.Feature": { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 9e6acc9d91580..ff48659b22dad 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1854,6 +1854,9 @@ const ( // quiet hours instead of max_ttl. ExperimentTemplateRestartRequirement Experiment = "template_restart_requirement" + // Insights page + ExperimentTemplateInsightsPage Experiment = "template_insights_page" + // Add new experiments here! // ExperimentExample Experiment = "example" ) @@ -1862,7 +1865,9 @@ const ( // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should // not be included here and will be essentially hidden. -var ExperimentsAll = Experiments{} +var ExperimentsAll = Experiments{ + ExperimentTemplateInsightsPage, +} // Experiments is a list of experiments that are enabled for the deployment. // Multiple experiments may be enabled at the same time. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index a8cee1c174fce..1ae74cfa3ad1f 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2610,6 +2610,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `convert-to-oidc` | | `single_tailnet` | | `template_restart_requirement` | +| `template_insights_page` | ## codersdk.Feature diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4bc840801994e..96a0108e4216a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1547,6 +1547,7 @@ export type Experiment = | "moons" | "single_tailnet" | "tailnet_ha_coordinator" + | "template_insights_page" | "template_restart_requirement" | "workspace_actions" export const Experiments: Experiment[] = [ @@ -1554,6 +1555,7 @@ export const Experiments: Experiment[] = [ "moons", "single_tailnet", "tailnet_ha_coordinator", + "template_insights_page", "template_restart_requirement", "workspace_actions", ] From c5efc1ee9fc59bf91425b26e27fdc3a3c88f420b Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 17:13:10 +0000 Subject: [PATCH 03/24] fix(site): fix error 'Reduce of empty array with no initial value' --- site/src/contexts/useProxyLatency.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts index 4ef0d8a4bdea7..5c5934acb4851 100644 --- a/site/src/contexts/useProxyLatency.ts +++ b/site/src/contexts/useProxyLatency.ts @@ -98,7 +98,11 @@ export const useProxyLatency = ( // This prevents fetching latencies too often. // 1. Fetch the latest stored latency for the given proxy. // 2. If the latest latency is after the latestFetchRequest, then skip the latency check. - if (storedLatencies && storedLatencies[proxy.id]) { + if ( + storedLatencies && + storedLatencies[proxy.id] && + storedLatencies[proxy.id].length > 0 + ) { const fetchRequestDate = new Date(latestFetchRequest) const latest = storedLatencies[proxy.id].reduce((prev, next) => prev.at > next.at ? prev : next, From fdcf23fa6cf47fd35860e82bc7c9e17e71c021c3 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 17:18:18 +0000 Subject: [PATCH 04/24] Show or not insights page --- .../TemplateLayout/TemplateLayout.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index 21a6c68764f10..4cddee463c591 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -15,6 +15,7 @@ import { import { useQuery } from "@tanstack/react-query" import { AuthorizationRequest } from "api/typesGenerated" import { ErrorAlert } from "components/Alert/ErrorAlert" +import { useDashboard } from "components/Dashboard/DashboardProvider" const templatePermissions = ( templateId: string, @@ -71,6 +72,12 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ queryKey: ["template", templateName], queryFn: () => fetchTemplate(orgId, templateName), }) + const dashboard = useDashboard() + const hasInsightsEnabled = + dashboard.experiments.includes("template_insights_page") || + process.env.NODE_ENV === "development" + const shouldShowInsights = + hasInsightsEnabled && data?.permissions?.canUpdateTemplate if (error) { return ( @@ -157,17 +164,19 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ > Embed - - combineClasses([ - styles.tabItem, - isActive ? styles.tabItemActive : undefined, - ]) - } - > - Insights - + {shouldShowInsights && ( + + combineClasses([ + styles.tabItem, + isActive ? styles.tabItemActive : undefined, + ]) + } + > + Insights + + )} From dfd2f031a03445fc7f5e9a7c792f11038725996f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 17:33:45 +0000 Subject: [PATCH 05/24] Set basic grid for the panels --- .../TemplateInsightsPage.tsx | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index d9b2f7808a1e4..c7b400e50bed9 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -1,5 +1,42 @@ +import Box from "@mui/material/Box" +import { styled } from "@mui/material/styles" + const TemplateInsightsPage = () => { - return
TemplateInsightsPage
+ return ( + theme.spacing(3), + }} + > + + Active daily users + + + + Latency by user + + + + App‘s & IDE usage + + + ) } export default TemplateInsightsPage + +const Panel = styled(Box)(({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + backgroundColor: theme.palette.background.paper, +})) + +const PanelHeader = styled(Box)(({ theme }) => ({ + fontSize: 14, + fontWeight: 500, + padding: theme.spacing(3), + lineHeight: 1, +})) From b40bdaee0370563a2077db774c031ddae542da43 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 18:03:48 +0000 Subject: [PATCH 06/24] Add DAU panel --- .../src/components/DAUChart/DAUChart.test.tsx | 37 --------- site/src/components/DAUChart/DAUChart.tsx | 72 ++++------------- .../TemplateInsightsPage.tsx | 43 +++++++++- .../TemplateSummaryPage.tsx | 13 +-- .../TemplateSummaryPageView.stories.tsx | 80 ++++++++----------- .../TemplateSummaryPageView.tsx | 11 +-- .../TemplatePage/TemplateSummaryPage/data.ts | 31 ------- 7 files changed, 101 insertions(+), 186 deletions(-) delete mode 100644 site/src/components/DAUChart/DAUChart.test.tsx delete mode 100644 site/src/pages/TemplatePage/TemplateSummaryPage/data.ts diff --git a/site/src/components/DAUChart/DAUChart.test.tsx b/site/src/components/DAUChart/DAUChart.test.tsx deleted file mode 100644 index 6a6866a5d095d..0000000000000 --- a/site/src/components/DAUChart/DAUChart.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { render } from "testHelpers/renderHelpers" -import { DAUChart, Language } from "./DAUChart" - -import { screen } from "@testing-library/react" -import { ResizeObserver } from "resize-observer" - -// The Chart performs dynamic resizes which fail in tests without this. -Object.defineProperty(window, "ResizeObserver", { - value: ResizeObserver, -}) - -describe("DAUChart", () => { - it("renders a helpful paragraph on empty state", async () => { - render( - , - ) - - await screen.findAllByText(Language.loadingText) - }) - it("renders a graph", async () => { - render( - , - ) - - await screen.findAllByText(Language.chartTitle) - }) -}) diff --git a/site/src/components/DAUChart/DAUChart.tsx b/site/src/components/DAUChart/DAUChart.tsx index 2b9c508b2060b..b3fecf965808b 100644 --- a/site/src/components/DAUChart/DAUChart.tsx +++ b/site/src/components/DAUChart/DAUChart.tsx @@ -15,13 +15,6 @@ import { Tooltip, } from "chart.js" import "chartjs-adapter-date-fns" -import { Stack } from "components/Stack/Stack" -import { - HelpTooltip, - HelpTooltipText, - HelpTooltipTitle, -} from "components/Tooltips/HelpTooltip" -import { WorkspaceSection } from "components/WorkspaceSection/WorkspaceSection" import dayjs from "dayjs" import { FC } from "react" import { Line } from "react-chartjs-2" @@ -40,24 +33,10 @@ ChartJS.register( export interface DAUChartProps { daus: TypesGen.DAUsResponse } -export const Language = { - loadingText: "DAU stats are loading. Check back later.", - chartTitle: "Daily Active Users", -} export const DAUChart: FC = ({ daus }) => { const theme: Theme = useTheme() - if (daus.entries.length === 0) { - return ( - // We generate hidden element to prove this path is taken in the test - // and through site inspection. -
-

{Language.loadingText}

-
- ) - } - const labels = daus.entries.map((val) => { return dayjs(val.date).format("YYYY-MM-DD") }) @@ -92,42 +71,25 @@ export const DAUChart: FC = ({ daus }) => { }, }, }, - aspectRatio: 10 / 1, + maintainAspectRatio: false, } return ( - <> - - {Language.chartTitle} - - How do we calculate DAUs? - - We use all workspace connection traffic to calculate DAUs. - - - - } - > - - - + ) } diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index c7b400e50bed9..a4005aa9d9d0d 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -1,5 +1,15 @@ import Box from "@mui/material/Box" import { styled } from "@mui/material/styles" +import { BoxProps } from "@mui/system" +import { useQuery } from "@tanstack/react-query" +import { getTemplateDAUs } from "api/api" +import { DAUChart } from "components/DAUChart/DAUChart" +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" +import { + HelpTooltip, + HelpTooltipTitle, + HelpTooltipText, +} from "components/Tooltips/HelpTooltip" const TemplateInsightsPage = () => { return ( @@ -11,9 +21,7 @@ const TemplateInsightsPage = () => { gap: (theme) => theme.spacing(3), }} > - - Active daily users - + Latency by user @@ -28,10 +36,34 @@ const TemplateInsightsPage = () => { export default TemplateInsightsPage +const DailyUsersPanel = (props: BoxProps) => { + const { template } = useTemplateLayoutContext() + const { data } = useQuery({ + queryKey: ["templates", template.id, "dau"], + queryFn: () => getTemplateDAUs(template.id), + }) + return ( + + + Active daily users + + How do we calculate DAUs? + + We use all workspace connection traffic to calculate DAUs. + + + + {data && } + + ) +} + const Panel = styled(Box)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, border: `1px solid ${theme.palette.divider}`, backgroundColor: theme.palette.background.paper, + display: "flex", + flexDirection: "column", })) const PanelHeader = styled(Box)(({ theme }) => ({ @@ -40,3 +72,8 @@ const PanelHeader = styled(Box)(({ theme }) => ({ padding: theme.spacing(3), lineHeight: 1, })) + +const PanelContent = styled(Box)(({ theme }) => ({ + padding: theme.spacing(0, 3, 3), + flex: 1, +})) diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx index 34faa71dd05fd..e169c42591e54 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx @@ -2,15 +2,16 @@ import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayo import { FC } from "react" import { Helmet } from "react-helmet-async" import { getTemplatePageTitle } from "../utils" -import { useTemplateSummaryData } from "./data" import { TemplateSummaryPageView } from "./TemplateSummaryPageView" +import { useQuery } from "@tanstack/react-query" +import { getTemplateVersionResources } from "api/api" export const TemplateSummaryPage: FC = () => { const { template, activeVersion } = useTemplateLayoutContext() - const { data } = useTemplateSummaryData( - template.id, - template.active_version_id, - ) + const { data: resources } = useQuery({ + queryKey: ["templates", template.id, "resources"], + queryFn: () => getTemplateVersionResources(activeVersion.id), + }) return ( <> @@ -18,7 +19,7 @@ export const TemplateSummaryPage: FC = () => { {getTemplatePageTitle("Template", template)} diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx index 58f47a8c5a1de..516d70362180e 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx @@ -1,78 +1,66 @@ -import { Story } from "@storybook/react" +import { Meta, StoryObj } from "@storybook/react" import { MockTemplate, - MockTemplateDAUResponse, MockTemplateVersion, MockTemplateVersion3, MockWorkspaceResource, MockWorkspaceResource2, } from "testHelpers/entities" -import { - TemplateSummaryPageView, - TemplateSummaryPageViewProps, -} from "./TemplateSummaryPageView" +import { TemplateSummaryPageView } from "./TemplateSummaryPageView" -export default { +const meta: Meta = { title: "pages/TemplateSummaryPageView", component: TemplateSummaryPageView, } -const Template: Story = (args) => ( - -) +export default meta +type Story = StoryObj -export const Example = Template.bind({}) -Example.args = { - template: MockTemplate, - activeVersion: MockTemplateVersion, - data: { +export const Example: Story = { + args: { + template: MockTemplate, + activeVersion: MockTemplateVersion, resources: [MockWorkspaceResource, MockWorkspaceResource2], - daus: MockTemplateDAUResponse, }, } -export const NoIcon = Template.bind({}) -NoIcon.args = { - template: { ...MockTemplate, icon: "" }, - activeVersion: MockTemplateVersion, - data: { +export const NoIcon: Story = { + args: { + template: { ...MockTemplate, icon: "" }, + activeVersion: MockTemplateVersion, resources: [MockWorkspaceResource, MockWorkspaceResource2], - daus: MockTemplateDAUResponse, }, } -export const SmallViewport = Template.bind({}) -SmallViewport.args = { - template: MockTemplate, - activeVersion: { - ...MockTemplateVersion, - readme: `--- - name:Template test - --- - ## Instructions - You can add instructions here +export const SmallViewport: Story = { + args: { + template: MockTemplate, + activeVersion: { + ...MockTemplateVersion, + readme: `--- + name:Template test + --- + ## Instructions + You can add instructions here - [Some link info](https://coder.com) - \`\`\` - # This is a really long sentence to test that the code block wraps into a new line properly. - \`\`\` - `, - }, - data: { + [Some link info](https://coder.com) + \`\`\` + # This is a really long sentence to test that the code block wraps into a new line properly. + \`\`\` + `, + }, resources: [MockWorkspaceResource, MockWorkspaceResource2], - daus: MockTemplateDAUResponse, }, } + SmallViewport.parameters = { chromatic: { viewports: [600] }, } -export const WithDeprecatedParameters = Template.bind({}) -WithDeprecatedParameters.args = { - template: MockTemplate, - activeVersion: MockTemplateVersion3, - data: { +export const WithDeprecatedParameters: Story = { + args: { + template: MockTemplate, + activeVersion: MockTemplateVersion3, resources: [MockWorkspaceResource, MockWorkspaceResource2], - daus: MockTemplateDAUResponse, }, } diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx index 462b9db37e69a..06eefa264a209 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx @@ -8,19 +8,17 @@ import { Stack } from "components/Stack/Stack" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" import { TemplateStats } from "components/TemplateStats/TemplateStats" import { FC, useEffect } from "react" -import { DAUChart } from "../../../components/DAUChart/DAUChart" -import { TemplateSummaryData } from "./data" import { useLocation, useNavigate } from "react-router-dom" import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings" export interface TemplateSummaryPageViewProps { - data?: TemplateSummaryData + resources?: WorkspaceResource[] template: Template activeVersion: TemplateVersion } export const TemplateSummaryPageView: FC = ({ - data, + resources, template, activeVersion, }) => { @@ -35,12 +33,10 @@ export const TemplateSummaryPageView: FC = ({ } }, [template, navigate, location]) - if (!data) { + if (!resources) { return } - const { daus, resources } = data - const getStartedResources = (resources: WorkspaceResource[]) => { return resources.filter( (resource) => resource.workspace_transition === "start", @@ -51,7 +47,6 @@ export const TemplateSummaryPageView: FC = ({ - {daus && } ) diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts b/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts deleted file mode 100644 index f9457d7549c6c..0000000000000 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useQuery } from "@tanstack/react-query" -import { getTemplateVersionResources, getTemplateDAUs } from "api/api" - -const fetchTemplateSummary = async ( - templateId: string, - activeVersionId: string, -) => { - const [resources, daus] = await Promise.all([ - getTemplateVersionResources(activeVersionId), - getTemplateDAUs(templateId), - ]) - - return { - resources, - daus, - } -} - -export const useTemplateSummaryData = ( - templateId: string, - activeVersionId: string, -) => { - return useQuery({ - queryKey: ["template", templateId, "summary"], - queryFn: () => fetchTemplateSummary(templateId, activeVersionId), - }) -} - -export type TemplateSummaryData = Awaited< - ReturnType -> From 6679ae31c6174d2660244179c13f928918b27392 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 18:54:30 +0000 Subject: [PATCH 07/24] Add avatar url into user latency insihgts --- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ coderd/database/queries.sql.go | 13 ++++++++----- coderd/database/queries/insights.sql | 1 + coderd/insights.go | 1 + codersdk/insights.go | 1 + docs/api/insights.md | 1 + docs/api/schemas.md | 4 ++++ site/src/api/api.ts | 10 ++++++++++ site/src/api/typesGenerated.ts | 1 + 10 files changed, 33 insertions(+), 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 58beeeed69e16..e44ea78b8c390 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9918,6 +9918,9 @@ const docTemplate = `{ "codersdk.UserLatency": { "type": "object", "properties": { + "avatar_url": { + "type": "string" + }, "latency_ms": { "$ref": "#/definitions/codersdk.ConnectionLatency" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 51e30be4b51dc..dbca672daf08d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8968,6 +8968,9 @@ "codersdk.UserLatency": { "type": "object", "properties": { + "avatar_url": { + "type": "string" + }, "latency_ms": { "$ref": "#/definitions/codersdk.ConnectionLatency" }, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 120edc4eb949c..2371d98d726d7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1545,6 +1545,7 @@ const getUserLatencyInsights = `-- name: GetUserLatencyInsights :many SELECT workspace_agent_stats.user_id, users.username, + users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 @@ -1567,11 +1568,12 @@ type GetUserLatencyInsightsParams struct { } type GetUserLatencyInsightsRow struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - Username string `db:"username" json:"username"` - TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"` - WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"` - WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + Username string `db:"username" json:"username"` + AvatarURL sql.NullString `db:"avatar_url" json:"avatar_url"` + TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"` + WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"` + WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"` } // GetUserLatencyInsights returns the median and 95th percentile connection @@ -1590,6 +1592,7 @@ func (q *sqlQuerier) GetUserLatencyInsights(ctx context.Context, arg GetUserLate if err := rows.Scan( &i.UserID, &i.Username, + &i.AvatarURL, pq.Array(&i.TemplateIDs), &i.WorkspaceConnectionLatency50, &i.WorkspaceConnectionLatency95, diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index e611ab209ff68..90e9ad1e79c26 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -6,6 +6,7 @@ SELECT workspace_agent_stats.user_id, users.username, + users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 diff --git a/coderd/insights.go b/coderd/insights.go index 3da60a13bfe84..f5f5b4ab2fd19 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -117,6 +117,7 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) { TemplateIDs: row.TemplateIDs, UserID: row.UserID, Username: row.Username, + AvatarURL: row.AvatarURL.String, LatencyMS: codersdk.ConnectionLatency{ P50: row.WorkspaceConnectionLatency50, P95: row.WorkspaceConnectionLatency95, diff --git a/codersdk/insights.go b/codersdk/insights.go index fb1c582c686c8..3c606920b1aac 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -44,6 +44,7 @@ type UserLatency struct { TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"` UserID uuid.UUID `json:"user_id" format:"uuid"` Username string `json:"username"` + AvatarURL string `json:"avatar_url"` LatencyMS ConnectionLatency `json:"latency_ms"` } diff --git a/docs/api/insights.md b/docs/api/insights.md index a802916fa579c..961f6fd25c4b9 100644 --- a/docs/api/insights.md +++ b/docs/api/insights.md @@ -117,6 +117,7 @@ curl -X GET http://coder-server:8080/api/v2/insights/user-latency \ "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "users": [ { + "avatar_url": "string", "latency_ms": { "p50": 31.312, "p95": 119.832 diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 1ae74cfa3ad1f..9dd1b53a70c9e 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -4880,6 +4880,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "avatar_url": "string", "latency_ms": { "p50": 31.312, "p95": 119.832 @@ -4894,6 +4895,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | Name | Type | Required | Restrictions | Description | | -------------- | -------------------------------------------------------- | -------- | ------------ | ----------- | +| `avatar_url` | string | false | | | | `latency_ms` | [codersdk.ConnectionLatency](#codersdkconnectionlatency) | false | | | | `template_ids` | array of string | false | | | | `user_id` | string | false | | | @@ -4908,6 +4910,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "users": [ { + "avatar_url": "string", "latency_ms": { "p50": 31.312, "p95": 119.832 @@ -4939,6 +4942,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "users": [ { + "avatar_url": "string", "latency_ms": { "p50": 31.312, "p95": 119.832 diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ba412e39e8764..f40781afbda2f 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1368,3 +1368,13 @@ export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { buildParameters, } } + +export const getInsightsUserLatency = async (filters: { + start_time: string + end_time: string + template_ids: string +}): Promise => { + const params = new URLSearchParams(filters) + const response = await axios.get(`/api/v2/insights/user-latency?${params}`) + return response.data +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 96a0108e4216a..3c96d592c97b0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1176,6 +1176,7 @@ export interface UserLatency { readonly template_ids: string[] readonly user_id: string readonly username: string + readonly avatar_url: string readonly latency_ms: ConnectionLatency } From e35a3b5a2aef0cf44b3b287c0d23bd00f0eb72f8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 19:11:11 +0000 Subject: [PATCH 08/24] List latency by user --- site/src/api/api.ts | 2 +- .../TemplateInsightsPage.tsx | 71 ++++++++++++++++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index f40781afbda2f..a5db89459dbd2 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1373,7 +1373,7 @@ export const getInsightsUserLatency = async (filters: { start_time: string end_time: string template_ids: string -}): Promise => { +}): Promise => { const params = new URLSearchParams(filters) const response = await axios.get(`/api/v2/insights/user-latency?${params}`) return response.data diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index a4005aa9d9d0d..3af0114bc9127 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box" -import { styled } from "@mui/material/styles" +import { styled, useTheme } from "@mui/material/styles" import { BoxProps } from "@mui/system" import { useQuery } from "@tanstack/react-query" -import { getTemplateDAUs } from "api/api" +import { getInsightsUserLatency, getTemplateDAUs } from "api/api" import { DAUChart } from "components/DAUChart/DAUChart" import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" import { @@ -10,8 +10,9 @@ import { HelpTooltipTitle, HelpTooltipText, } from "components/Tooltips/HelpTooltip" +import { getLatencyColor } from "utils/latency" -const TemplateInsightsPage = () => { +export default function TemplateInsightsPage() { return ( { }} > - - - Latency by user - + App‘s & IDE usage @@ -34,8 +32,6 @@ const TemplateInsightsPage = () => { ) } -export default TemplateInsightsPage - const DailyUsersPanel = (props: BoxProps) => { const { template } = useTemplateLayoutContext() const { data } = useQuery({ @@ -58,6 +54,48 @@ const DailyUsersPanel = (props: BoxProps) => { ) } +const UserLatencyPanel = (props: BoxProps) => { + const { template } = useTemplateLayoutContext() + const { data } = useQuery({ + queryKey: ["templates", template.id, "user-latency"], + queryFn: () => + getInsightsUserLatency({ + template_ids: template.id, + start_time: toTimeFilter(getTimeFor7DaysAgo()), + end_time: toTimeFilter(new Date()), + }), + }) + const theme = useTheme() + + return ( + + + Latency by user + + + {data && + data.report.users + .sort((a, b) => b.latency_ms.p95 - a.latency_ms.p95) + .map((row) => ( + + {row.username} + + {row.latency_ms.p95.toFixed(0)}ms + + + ))} + + + ) +} + const Panel = styled(Box)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, border: `1px solid ${theme.palette.divider}`, @@ -77,3 +115,18 @@ const PanelContent = styled(Box)(({ theme }) => ({ padding: theme.spacing(0, 3, 3), flex: 1, })) + +function getTimeFor7DaysAgo(): Date { + const sevenDaysAgo = new Date() + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7) + return sevenDaysAgo +} + +function toTimeFilter(date: Date) { + date.setHours(0, 0, 0, 0) + const year = date.getUTCFullYear() + const month = String(date.getUTCMonth() + 1).padStart(2, "0") + const day = String(date.getUTCDate()).padStart(2, "0") + + return `${year}-${month}-${day}T00:00:00Z` +} From 544590d18d5022270a6a544a55bfa50a5ebd45af Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 24 Jul 2023 19:16:01 +0000 Subject: [PATCH 09/24] Add user avatar to the list --- .../TemplateInsightsPage.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 3af0114bc9127..6e9b556fae34a 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -10,6 +10,7 @@ import { HelpTooltipTitle, HelpTooltipText, } from "components/Tooltips/HelpTooltip" +import { UserAvatar } from "components/UserAvatar/UserAvatar" import { getLatencyColor } from "utils/latency" export default function TemplateInsightsPage() { @@ -83,10 +84,21 @@ const UserLatencyPanel = (props: BoxProps) => { display: "flex", justifyContent: "space-between", fontSize: 14, + py: 1, }} > - {row.username} - + + + {row.username} + + {row.latency_ms.p95.toFixed(0)}ms From 1a324029e7cbb5367dbce983b4c41cc3bb875504 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 12:12:27 +0000 Subject: [PATCH 10/24] Use avatar url --- .../TemplateInsightsPage/TemplateInsightsPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 6e9b556fae34a..614aafd2ac2eb 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -88,7 +88,10 @@ const UserLatencyPanel = (props: BoxProps) => { }} > - + {row.username} Date: Tue, 25 Jul 2023 12:29:01 +0000 Subject: [PATCH 11/24] Fix icon url --- cli/templateedit_test.go | 4 ++-- coderd/insights.go | 8 ++++---- coderd/templates_test.go | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index f88bb230926a8..3c60ad327abea 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -40,7 +40,7 @@ func TestTemplateEdit(t *testing.T) { name := "new-template-name" displayName := "New Display Name 789" desc := "lorem ipsum dolor sit amet et cetera" - icon := "/icons/new-icon.png" + icon := "/icon/new-icon.png" defaultTTL := 12 * time.Hour allowUserCancelWorkspaceJobs := false @@ -168,7 +168,7 @@ func TestTemplateEdit(t *testing.T) { // Test the cli command. displayName := "New Display Name 789" description := "New Description ABC" - icon := "/icons/new-icon.png" + icon := "/icon/new-icon.png" cmdArgs := []string{ "templates", "edit", diff --git a/coderd/insights.go b/coderd/insights.go index f5f5b4ab2fd19..2c6d22be0ab53 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -257,7 +257,7 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [ Type: codersdk.TemplateAppsTypeBuiltin, DisplayName: "Visual Studio Code", Slug: "vscode", - Icon: "/icons/code.svg", + Icon: "/icon/code.svg", Seconds: usage.UsageVscodeSeconds, }, { @@ -265,7 +265,7 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [ Type: codersdk.TemplateAppsTypeBuiltin, DisplayName: "JetBrains", Slug: "jetbrains", - Icon: "/icons/intellij.svg", + Icon: "/icon/intellij.svg", Seconds: usage.UsageJetbrainsSeconds, }, { @@ -273,7 +273,7 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [ Type: codersdk.TemplateAppsTypeBuiltin, DisplayName: "Web Terminal", Slug: "reconnecting-pty", - Icon: "/icons/terminal.svg", + Icon: "/icon/terminal.svg", Seconds: usage.UsageReconnectingPtySeconds, }, { @@ -281,7 +281,7 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [ Type: codersdk.TemplateAppsTypeBuiltin, DisplayName: "SSH", Slug: "ssh", - Icon: "/icons/terminal.svg", + Icon: "/icon/terminal.svg", Seconds: usage.UsageSshSeconds, }, } diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 0989e93287c11..2b7c41c9ad8f8 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -478,7 +478,7 @@ func TestPatchTemplateMeta(t *testing.T) { Name: "new-template-name", DisplayName: "Displayed Name 456", Description: "lorem ipsum dolor sit amet et cetera", - Icon: "/icons/new-icon.png", + Icon: "/icon/new-icon.png", DefaultTTLMillis: 12 * time.Hour.Milliseconds(), AllowUserCancelWorkspaceJobs: false, } @@ -883,7 +883,7 @@ func TestPatchTemplateMeta(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Description = "original description" - ctr.Icon = "/icons/original-icon.png" + ctr.Icon = "/icon/original-icon.png" ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) }) @@ -950,7 +950,7 @@ func TestPatchTemplateMeta(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.Icon = "/icons/code.png" + ctr.Icon = "/icon/code.png" }) req := codersdk.UpdateTemplateMeta{ Icon: "", From 864c6dd5c516a7e6e2e85a5e8c3c52b702546411 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 13:11:30 +0000 Subject: [PATCH 12/24] Add apps and IDE usage --- package-lock.json | 39 ++++++ package.json | 8 ++ site/src/api/api.ts | 16 ++- .../TemplateInsightsPage.tsx | 123 +++++++++++++++++- yarn.lock | 13 ++ 5 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 yarn.lock diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000..a5cb9c06471f0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,39 @@ +{ + "name": "coder", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "chroma-js": "^2.4.2" + }, + "devDependencies": { + "@types/chroma-js": "^2.4.0" + } + }, + "node_modules/@types/chroma-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz", + "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==", + "dev": true + }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + } + }, + "dependencies": { + "@types/chroma-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz", + "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==", + "dev": true + }, + "chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000..3a8b576731e7d --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "chroma-js": "^2.4.2" + }, + "devDependencies": { + "@types/chroma-js": "^2.4.0" + } +} diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a5db89459dbd2..d0526398550d5 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1369,12 +1369,24 @@ export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { } } -export const getInsightsUserLatency = async (filters: { +type InsightsFilter = { start_time: string end_time: string template_ids: string -}): Promise => { +} + +export const getInsightsUserLatency = async ( + filters: InsightsFilter, +): Promise => { const params = new URLSearchParams(filters) const response = await axios.get(`/api/v2/insights/user-latency?${params}`) return response.data } + +export const getInsightsTemplate = async ( + filters: InsightsFilter, +): Promise => { + const params = new URLSearchParams(filters) + const response = await axios.get(`/api/v2/insights/templates?${params}`) + return response.data +} diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 614aafd2ac2eb..555496c91145b 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -1,8 +1,13 @@ +import LinearProgress from "@mui/material/LinearProgress" import Box from "@mui/material/Box" import { styled, useTheme } from "@mui/material/styles" import { BoxProps } from "@mui/system" import { useQuery } from "@tanstack/react-query" -import { getInsightsUserLatency, getTemplateDAUs } from "api/api" +import { + getInsightsTemplate, + getInsightsUserLatency, + getTemplateDAUs, +} from "api/api" import { DAUChart } from "components/DAUChart/DAUChart" import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" import { @@ -12,6 +17,8 @@ import { } from "components/Tooltips/HelpTooltip" import { UserAvatar } from "components/UserAvatar/UserAvatar" import { getLatencyColor } from "utils/latency" +import chroma from "chroma-js" +import { colors } from "theme/colors" export default function TemplateInsightsPage() { return ( @@ -25,10 +32,7 @@ export default function TemplateInsightsPage() { > - - - App‘s & IDE usage - + ) } @@ -111,6 +115,98 @@ const UserLatencyPanel = (props: BoxProps) => { ) } +const TemplateUsagePanel = (props: BoxProps) => { + const { template } = useTemplateLayoutContext() + const { data } = useQuery({ + queryKey: ["templates", template.id, "usage"], + queryFn: () => + getInsightsTemplate({ + template_ids: template.id, + start_time: toTimeFilter(getTimeFor7DaysAgo()), + end_time: toTimeFilter(new Date()), + }), + }) + const totalInSeconds = + data?.report.apps_usage.reduce( + (total, usage) => total + usage.seconds, + 0, + ) ?? 1 + const usageColors = chroma + .scale([colors.green[8], colors.blue[8]]) + .mode("lch") + .colors(data?.report.apps_usage.length ?? 0) + return ( + + App‘s & IDE usage + + {data && ( + + {data.report.apps_usage + .sort((a, b) => b.seconds - a.seconds) + .map((usage, i) => { + const percentage = (usage.seconds / totalInSeconds) * 100 + return ( + + + + + + + {usage.slug} + + + theme.palette.divider, + "& .MuiLinearProgress-bar": { + backgroundColor: usageColors[i], + borderRadius: 999, + }, + }} + /> + theme.palette.text.secondary, + width: 200, + flexShrink: 0, + }} + > + {formatTime(usage.seconds)} + + + ) + })} + + )} + + + ) +} + const Panel = styled(Box)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, border: `1px solid ${theme.palette.divider}`, @@ -145,3 +241,20 @@ function toTimeFilter(date: Date) { return `${year}-${month}-${day}T00:00:00Z` } + +function formatTime(seconds: number): string { + if (seconds < 60) { + return seconds + " seconds" + } else if (seconds >= 60 && seconds < 3600) { + const minutes = Math.floor(seconds / 60) + return minutes + " minutes" + } else { + const hours = Math.floor(seconds / 3600) + const remainingMinutes = Math.floor((seconds % 3600) / 60) + if (remainingMinutes === 0) { + return hours + " hours" + } else { + return hours + " hours, " + remainingMinutes + " minutes" + } + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000000..3dac3b6786cd7 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,13 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/chroma-js@^2.4.0": + "integrity" "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==" + "resolved" "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz" + "version" "2.4.0" + +"chroma-js@^2.4.2": + "integrity" "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + "resolved" "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz" + "version" "2.4.2" From 7ea9a26047107668c16630088fd8cb3a65c52b2b Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 13:20:03 +0000 Subject: [PATCH 13/24] Fix title --- .../TemplateInsightsPage.tsx | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 555496c91145b..dc265812cd268 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -19,21 +19,31 @@ import { UserAvatar } from "components/UserAvatar/UserAvatar" import { getLatencyColor } from "utils/latency" import chroma from "chroma-js" import { colors } from "theme/colors" +import { Helmet } from "react-helmet-async" +import { getTemplatePageTitle } from "../utils" export default function TemplateInsightsPage() { + const { template } = useTemplateLayoutContext() + return ( - theme.spacing(3), - }} - > - - - - + <> + + {getTemplatePageTitle("Insights", template)} + + + theme.spacing(3), + }} + > + + + + + ) } From 2aafcee66649aab5af9a854a84fd764745cdac6f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 13:51:55 +0000 Subject: [PATCH 14/24] Add load and empty states --- site/src/components/Loader/Loader.tsx | 6 ++- .../TemplateInsightsPage.tsx | 40 +++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/site/src/components/Loader/Loader.tsx b/site/src/components/Loader/Loader.tsx index 39afe9e885469..65cc9d474da1a 100644 --- a/site/src/components/Loader/Loader.tsx +++ b/site/src/components/Loader/Loader.tsx @@ -1,9 +1,10 @@ -import Box from "@mui/material/Box" +import Box, { BoxProps } from "@mui/material/Box" import CircularProgress from "@mui/material/CircularProgress" import { FC } from "react" -export const Loader: FC> = ({ +export const Loader: FC<{ size?: number } & BoxProps> = ({ size = 26, + ...boxProps }) => { return ( > = ({ alignItems="center" justifyContent="center" data-testid="loader" + {...boxProps} > diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index dc265812cd268..629c956873912 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -21,6 +21,7 @@ import chroma from "chroma-js" import { colors } from "theme/colors" import { Helmet } from "react-helmet-async" import { getTemplatePageTitle } from "../utils" +import { Loader } from "components/Loader/Loader" export default function TemplateInsightsPage() { const { template } = useTemplateLayoutContext() @@ -64,7 +65,11 @@ const DailyUsersPanel = (props: BoxProps) => { - {data && } + + {!data && } + {data && data.entries.length === 0 && } + {data && } + ) } @@ -81,6 +86,7 @@ const UserLatencyPanel = (props: BoxProps) => { }), }) const theme = useTheme() + const users = data?.report.users return ( @@ -88,8 +94,10 @@ const UserLatencyPanel = (props: BoxProps) => { Latency by user - {data && - data.report.users + {!data && } + {users && users.length === 0 && } + {users && + users .sort((a, b) => b.latency_ms.p95 - a.latency_ms.p95) .map((row) => ( { .scale([colors.green[8], colors.blue[8]]) .mode("lch") .colors(data?.report.apps_usage.length ?? 0) + // The API returns a row for each app, even if the user didn't use it. + const hasDataAvailable = data?.report.apps_usage.some((u) => u.seconds > 0) return ( App‘s & IDE usage - {data && ( + {!data && } + {!hasDataAvailable && } + {data && hasDataAvailable && ( {data.report.apps_usage .sort((a, b) => b.seconds - a.seconds) @@ -237,6 +249,26 @@ const PanelContent = styled(Box)(({ theme }) => ({ flex: 1, })) +const NoDataAvailable = (props: BoxProps) => { + return ( + theme.palette.text.secondary, + textAlign: "center", + height: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + ...props.sx, + }} + > + No data available + + ) +} + function getTimeFor7DaysAgo(): Date { const sevenDaysAgo = new Date() sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7) From 941671b552ee8d56ce4b646e80040f218cfbc762 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 14:01:44 +0000 Subject: [PATCH 15/24] Update date range --- .../TemplateInsightsPage/TemplateInsightsPage.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 629c956873912..41481ff145a75 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -81,7 +81,7 @@ const UserLatencyPanel = (props: BoxProps) => { queryFn: () => getInsightsUserLatency({ template_ids: template.id, - start_time: toTimeFilter(getTimeFor7DaysAgo()), + start_time: toTimeFilter(new Date(template.created_at)), end_time: toTimeFilter(new Date()), }), }) @@ -140,7 +140,7 @@ const TemplateUsagePanel = (props: BoxProps) => { queryFn: () => getInsightsTemplate({ template_ids: template.id, - start_time: toTimeFilter(getTimeFor7DaysAgo()), + start_time: toTimeFilter(new Date(template.created_at)), end_time: toTimeFilter(new Date()), }), }) @@ -269,12 +269,6 @@ const NoDataAvailable = (props: BoxProps) => { ) } -function getTimeFor7DaysAgo(): Date { - const sevenDaysAgo = new Date() - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7) - return sevenDaysAgo -} - function toTimeFilter(date: Date) { date.setHours(0, 0, 0, 0) const year = date.getUTCFullYear() From 37da9b1352c03b8d16c5141905a1ed0ed995a840 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 14:14:01 +0000 Subject: [PATCH 16/24] Fix loading and not available data --- .../TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 41481ff145a75..c5bdb050bc833 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -160,7 +160,7 @@ const TemplateUsagePanel = (props: BoxProps) => { App‘s & IDE usage {!data && } - {!hasDataAvailable && } + {data && !hasDataAvailable && } {data && hasDataAvailable && ( {data.report.apps_usage From b8ce94ac4cef9aeb88e0f8249b3b291afc49c8cd Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 14:18:13 +0000 Subject: [PATCH 17/24] Fix data empty --- .../TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index c5bdb050bc833..0ed68539e48a2 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -68,7 +68,7 @@ const DailyUsersPanel = (props: BoxProps) => { {!data && } {data && data.entries.length === 0 && } - {data && } + {data && data.entries.length > 0 && } ) From 06178c9fc8b3a51437973166cf5fecfd4e70d579 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 14:33:50 +0000 Subject: [PATCH 18/24] feat(site): add terminal icon --- site/static/icon/terminal.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 site/static/icon/terminal.svg diff --git a/site/static/icon/terminal.svg b/site/static/icon/terminal.svg new file mode 100644 index 0000000000000..79cd03587673d --- /dev/null +++ b/site/static/icon/terminal.svg @@ -0,0 +1 @@ + From 8be0e42516f05ed5d8486f2be9a73824a176aa0f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 16:48:37 +0000 Subject: [PATCH 19/24] Consume DAUs from insights --- site/src/api/api.ts | 5 +- .../TemplateInsightsPage.tsx | 100 +++++++++++------- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index d0526398550d5..8aa392b49febb 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1386,7 +1386,10 @@ export const getInsightsUserLatency = async ( export const getInsightsTemplate = async ( filters: InsightsFilter, ): Promise => { - const params = new URLSearchParams(filters) + const params = new URLSearchParams({ + ...filters, + interval: "day", + }) const response = await axios.get(`/api/v2/insights/templates?${params}`) return response.data } diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 0ed68539e48a2..ada39030ae5f2 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -3,11 +3,7 @@ import Box from "@mui/material/Box" import { styled, useTheme } from "@mui/material/styles" import { BoxProps } from "@mui/system" import { useQuery } from "@tanstack/react-query" -import { - getInsightsTemplate, - getInsightsUserLatency, - getTemplateDAUs, -} from "api/api" +import { getInsightsTemplate, getInsightsUserLatency } from "api/api" import { DAUChart } from "components/DAUChart/DAUChart" import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" import { @@ -22,9 +18,21 @@ import { colors } from "theme/colors" import { Helmet } from "react-helmet-async" import { getTemplatePageTitle } from "../utils" import { Loader } from "components/Loader/Loader" +import { DAUsResponse, TemplateInsightsResponse } from "api/typesGenerated" +import { ComponentProps } from "react" +import subDays from "date-fns/subDays" export default function TemplateInsightsPage() { const { template } = useTemplateLayoutContext() + const { data: templateInsights } = useQuery({ + queryKey: ["templates", template.id, "usage"], + queryFn: () => + getInsightsTemplate({ + template_ids: template.id, + start_time: toTimeFilter(sevenDaysAgo()), + end_time: toTimeFilter(new Date()), + }), + }) return ( <> @@ -40,22 +48,28 @@ export default function TemplateInsightsPage() { gap: (theme) => theme.spacing(3), }} > - + - + ) } -const DailyUsersPanel = (props: BoxProps) => { - const { template } = useTemplateLayoutContext() - const { data } = useQuery({ - queryKey: ["templates", template.id, "dau"], - queryFn: () => getTemplateDAUs(template.id), - }) +const DailyUsersPanel = ({ + data, + ...panelProps +}: PanelProps & { + data: TemplateInsightsResponse["interval_reports"] | undefined +}) => { return ( - + Active daily users @@ -67,21 +81,21 @@ const DailyUsersPanel = (props: BoxProps) => { {!data && } - {data && data.entries.length === 0 && } - {data && data.entries.length > 0 && } + {data && data.length === 0 && } + {data && data.length > 0 && } ) } -const UserLatencyPanel = (props: BoxProps) => { +const UserLatencyPanel = (props: PanelProps) => { const { template } = useTemplateLayoutContext() const { data } = useQuery({ queryKey: ["templates", template.id, "user-latency"], queryFn: () => getInsightsUserLatency({ template_ids: template.id, - start_time: toTimeFilter(new Date(template.created_at)), + start_time: toTimeFilter(sevenDaysAgo()), end_time: toTimeFilter(new Date()), }), }) @@ -133,37 +147,29 @@ const UserLatencyPanel = (props: BoxProps) => { ) } -const TemplateUsagePanel = (props: BoxProps) => { - const { template } = useTemplateLayoutContext() - const { data } = useQuery({ - queryKey: ["templates", template.id, "usage"], - queryFn: () => - getInsightsTemplate({ - template_ids: template.id, - start_time: toTimeFilter(new Date(template.created_at)), - end_time: toTimeFilter(new Date()), - }), - }) +const TemplateUsagePanel = ({ + data, + ...panelProps +}: PanelProps & { + data: TemplateInsightsResponse["report"]["apps_usage"] | undefined +}) => { const totalInSeconds = - data?.report.apps_usage.reduce( - (total, usage) => total + usage.seconds, - 0, - ) ?? 1 + data?.reduce((total, usage) => total + usage.seconds, 0) ?? 1 const usageColors = chroma .scale([colors.green[8], colors.blue[8]]) .mode("lch") - .colors(data?.report.apps_usage.length ?? 0) + .colors(data?.length ?? 0) // The API returns a row for each app, even if the user didn't use it. - const hasDataAvailable = data?.report.apps_usage.some((u) => u.seconds > 0) + const hasDataAvailable = data?.some((u) => u.seconds > 0) return ( - + App‘s & IDE usage {!data && } {data && !hasDataAvailable && } {data && hasDataAvailable && ( - {data.report.apps_usage + {data .sort((a, b) => b.seconds - a.seconds) .map((usage, i) => { const percentage = (usage.seconds / totalInSeconds) * 100 @@ -237,6 +243,8 @@ const Panel = styled(Box)(({ theme }) => ({ flexDirection: "column", })) +type PanelProps = ComponentProps + const PanelHeader = styled(Box)(({ theme }) => ({ fontSize: 14, fontWeight: 500, @@ -269,6 +277,20 @@ const NoDataAvailable = (props: BoxProps) => { ) } +function mapToDAUsResponse( + data: TemplateInsightsResponse["interval_reports"], +): DAUsResponse { + return { + tz_hour_offset: 0, + entries: data.map((d) => { + return { + amount: d.active_users, + date: d.end_time, + } + }), + } +} + function toTimeFilter(date: Date) { date.setHours(0, 0, 0, 0) const year = date.getUTCFullYear() @@ -294,3 +316,7 @@ function formatTime(seconds: number): string { } } } + +function sevenDaysAgo() { + return subDays(new Date(), 7) +} From e2768e391c3a794eba2a60013f57ed3b2336195f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 17:32:19 +0000 Subject: [PATCH 20/24] Apply minor improvements --- .../TemplateInsightsPage/TemplateInsightsPage.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index ada39030ae5f2..5bd0dfd3b8825 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -71,7 +71,7 @@ const DailyUsersPanel = ({ return ( - Active daily users + Daily Active Users How do we calculate DAUs? @@ -153,14 +153,15 @@ const TemplateUsagePanel = ({ }: PanelProps & { data: TemplateInsightsResponse["report"]["apps_usage"] | undefined }) => { + const validUsage = data?.filter((u) => u.seconds > 0) const totalInSeconds = - data?.reduce((total, usage) => total + usage.seconds, 0) ?? 1 + validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1 const usageColors = chroma .scale([colors.green[8], colors.blue[8]]) .mode("lch") - .colors(data?.length ?? 0) + .colors(validUsage?.length ?? 0) // The API returns a row for each app, even if the user didn't use it. - const hasDataAvailable = data?.some((u) => u.seconds > 0) + const hasDataAvailable = validUsage && validUsage.length > 0 return ( App‘s & IDE usage @@ -169,7 +170,7 @@ const TemplateUsagePanel = ({ {data && !hasDataAvailable && } {data && hasDataAvailable && ( - {data + {validUsage .sort((a, b) => b.seconds - a.seconds) .map((usage, i) => { const percentage = (usage.seconds / totalInSeconds) * 100 @@ -199,7 +200,7 @@ const TemplateUsagePanel = ({ /> - {usage.slug} + {usage.display_name} Date: Tue, 25 Jul 2023 17:41:50 +0000 Subject: [PATCH 21/24] Minor improvements --- .../TemplateInsightsPage.tsx | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 5bd0dfd3b8825..02333d2d5ded0 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -70,14 +70,17 @@ const DailyUsersPanel = ({ }) => { return ( - - Daily Active Users - - How do we calculate DAUs? - - We use all workspace connection traffic to calculate DAUs. - - + + + Daily Active Users + + How do we calculate DAUs? + + We use all workspace connection traffic to calculate DAUs. + + + + Last 7 days {!data && } @@ -104,8 +107,17 @@ const UserLatencyPanel = (props: PanelProps) => { return ( - - Latency by user + + + Latency by user + + How do we calculate latency? + + The average latency of user connections to workspaces. + + + + Last 7 days {!data && } @@ -164,7 +176,10 @@ const TemplateUsagePanel = ({ const hasDataAvailable = validUsage && validUsage.length > 0 return ( - App‘s & IDE usage + + App‘s & IDE usage + Last 7 days + {!data && } {data && !hasDataAvailable && } @@ -247,10 +262,17 @@ const Panel = styled(Box)(({ theme }) => ({ type PanelProps = ComponentProps const PanelHeader = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2.5, 3, 3), +})) + +const PanelTitle = styled(Box)(() => ({ fontSize: 14, fontWeight: 500, - padding: theme.spacing(3), - lineHeight: 1, +})) + +const PanelSubtitle = styled(Box)(({ theme }) => ({ + fontSize: 13, + color: theme.palette.text.secondary, })) const PanelContent = styled(Box)(({ theme }) => ({ From f147a4d159e1e6b873224da48b37571044972d5f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 17:47:33 +0000 Subject: [PATCH 22/24] Fix deps --- package-lock.json | 39 --------------------------------------- package.json | 8 -------- site/package.json | 2 ++ site/yarn.lock | 10 ++++++++++ yarn.lock | 13 ------------- 5 files changed, 12 insertions(+), 60 deletions(-) delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 yarn.lock diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a5cb9c06471f0..0000000000000 --- a/package-lock.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "coder", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "chroma-js": "^2.4.2" - }, - "devDependencies": { - "@types/chroma-js": "^2.4.0" - } - }, - "node_modules/@types/chroma-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz", - "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==", - "dev": true - }, - "node_modules/chroma-js": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" - } - }, - "dependencies": { - "@types/chroma-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz", - "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==", - "dev": true - }, - "chroma-js": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 3a8b576731e7d..0000000000000 --- a/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "chroma-js": "^2.4.2" - }, - "devDependencies": { - "@types/chroma-js": "^2.4.0" - } -} diff --git a/site/package.json b/site/package.json index 25ff8959de6cf..6a8e6ae153658 100644 --- a/site/package.json +++ b/site/package.json @@ -56,6 +56,7 @@ "canvas": "2.11.0", "chart.js": "3.9.1", "chartjs-adapter-date-fns": "3.0.0", + "chroma-js": "2.4.2", "color-convert": "2.0.1", "cron-parser": "4.7.0", "cronstrue": "2.28.0", @@ -116,6 +117,7 @@ "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "14.0.0", "@testing-library/user-event": "14.4.3", + "@types/chroma-js": "2.4.0", "@types/jest": "29.5.2", "@types/node": "14.18.22", "@types/react": "18.2.6", diff --git a/site/yarn.lock b/site/yarn.lock index 4c2e6ecb5c922..bbbe6d3984c13 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -3506,6 +3506,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/chroma-js@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.4.0.tgz#476a16ae848c77478079d6749236fdb98837b92c" + integrity sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw== + "@types/color-convert@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" @@ -4879,6 +4884,11 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +chroma-js@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" + integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== + chromatic@6.20.0: version "6.20.0" resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-6.20.0.tgz#28dcbcc254e51bcb887d9b5203b3a11dd5ff890a" diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 3dac3b6786cd7..0000000000000 --- a/yarn.lock +++ /dev/null @@ -1,13 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/chroma-js@^2.4.0": - "integrity" "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==" - "resolved" "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz" - "version" "2.4.0" - -"chroma-js@^2.4.2": - "integrity" "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" - "resolved" "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz" - "version" "2.4.2" From 8ed5ca304e886d8e047af956fba2d676d9ef507e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Jul 2023 18:18:39 +0000 Subject: [PATCH 23/24] Add storybook --- .../TemplateInsightsPage.stories.tsx | 290 ++++++++++++++++++ .../TemplateInsightsPage.tsx | 91 +++--- 2 files changed, 343 insertions(+), 38 deletions(-) create mode 100644 site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx new file mode 100644 index 0000000000000..995e8e83b799e --- /dev/null +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -0,0 +1,290 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { TemplateInsightsPageView } from "./TemplateInsightsPage" + +const meta: Meta = { + title: "pages/TemplateInsightsPageView", + component: TemplateInsightsPageView, +} + +export default meta +type Story = StoryObj + +export const Loading: Story = { + args: { + templateInsights: undefined, + userLatency: undefined, + }, +} + +export const Empty: Story = { + args: { + templateInsights: { + interval_reports: [], + report: { + active_users: 0, + end_time: "", + start_time: "", + template_ids: [], + apps_usage: [], + }, + }, + userLatency: { + report: { + end_time: "", + start_time: "", + template_ids: [], + users: [], + }, + }, + }, +} + +export const Loaded: Story = { + args: { + // Got from dev.coder.com network calls + templateInsights: { + report: { + start_time: "2023-07-18T00:00:00Z", + end_time: "2023-07-25T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + active_users: 14, + apps_usage: [ + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "Visual Studio Code", + slug: "vscode", + icon: "/icon/code.svg", + seconds: 2513400, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "JetBrains", + slug: "jetbrains", + icon: "/icon/intellij.svg", + seconds: 0, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "Web Terminal", + slug: "reconnecting-pty", + icon: "/icon/terminal.svg", + seconds: 110400, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "SSH", + slug: "ssh", + icon: "/icon/terminal.svg", + seconds: 1020900, + }, + ], + }, + interval_reports: [ + { + start_time: "2023-07-18T00:00:00Z", + end_time: "2023-07-19T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 13, + }, + { + start_time: "2023-07-19T00:00:00Z", + end_time: "2023-07-20T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 11, + }, + { + start_time: "2023-07-20T00:00:00Z", + end_time: "2023-07-21T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 11, + }, + { + start_time: "2023-07-21T00:00:00Z", + end_time: "2023-07-22T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 13, + }, + { + start_time: "2023-07-22T00:00:00Z", + end_time: "2023-07-23T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 7, + }, + { + start_time: "2023-07-23T00:00:00Z", + end_time: "2023-07-24T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 5, + }, + { + start_time: "2023-07-24T00:00:00Z", + end_time: "2023-07-25T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 11, + }, + ], + }, + userLatency: { + report: { + start_time: "2023-07-18T00:00:00Z", + end_time: "2023-07-25T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + users: [ + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "0bac0dfd-b086-4b6d-b8ba-789e0eca7451", + username: "kylecarbs", + avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", + latency_ms: { + p50: 63.826, + p95: 139.328, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "12b03f43-1bb7-4fca-967a-585c97f31682", + username: "coadler", + avatar_url: "https://avatars.githubusercontent.com/u/6332295?v=4", + latency_ms: { + p50: 51.0745, + p95: 54.62562499999999, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "15890ddb-142c-443d-8fd5-cd8307256ab1", + username: "jsjoeio", + avatar_url: "https://avatars.githubusercontent.com/u/3806031?v=4", + latency_ms: { + p50: 37.444, + p95: 37.8488, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "3f8c0eef-6a45-4759-a4d6-d00bbffb1369", + username: "dean", + avatar_url: "https://avatars.githubusercontent.com/u/11241812?v=4", + latency_ms: { + p50: 7.1295, + p95: 70.34084999999999, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "59da0bfe-9c99-47fa-a563-f9fdb18449d0", + username: "cian", + avatar_url: + "https://lh3.googleusercontent.com/a/AAcHTtdsYrtIfkXU52rHXhY9DHehpw-slUKe9v6UELLJgXT2mDM=s96-c", + latency_ms: { + p50: 42.14975, + p95: 125.5441, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "5ccd3128-cbbb-4cfb-8139-5a1edbb60c71", + username: "bpmct", + avatar_url: "https://avatars.githubusercontent.com/u/22407953?v=4", + latency_ms: { + p50: 42.175, + p95: 43.437599999999996, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "631f78f6-098e-4cb0-ae4f-418fafb0a406", + username: "matifali", + avatar_url: "https://avatars.githubusercontent.com/u/10648092?v=4", + latency_ms: { + p50: 78.02, + p95: 86.3328, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "740bba7f-356d-4203-8f15-03ddee381998", + username: "eric", + avatar_url: "https://avatars.githubusercontent.com/u/9683576?v=4", + latency_ms: { + p50: 34.533, + p95: 110.52659999999999, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "78dd2361-4a5a-42b0-9ec3-3eea23af1094", + username: "code-asher", + avatar_url: "https://avatars.githubusercontent.com/u/45609798?v=4", + latency_ms: { + p50: 74.78875, + p95: 114.80699999999999, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "7f5cc5e9-20ee-48ce-959d-081b3f52273e", + username: "mafredri", + avatar_url: "https://avatars.githubusercontent.com/u/147409?v=4", + latency_ms: { + p50: 19.2115, + p95: 96.44249999999992, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "9ed91bb9-db45-4cef-b39c-819856e98c30", + username: "jon", + avatar_url: + "https://lh3.googleusercontent.com/a/AAcHTtddhPxiGYniy6_rFhdAi2C1YwKvDButlCvJ6G-166mG=s96-c", + latency_ms: { + p50: 42.0445, + p95: 133.846, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "a73425d1-53a7-43d3-b6ae-cae9ba59b92b", + username: "ammar", + avatar_url: "https://avatars.githubusercontent.com/u/7416144?v=4", + latency_ms: { + p50: 49.249, + p95: 56.773250000000004, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "af657bc3-6949-4b1b-bc2d-d41a40b546a4", + username: "BrunoQuaresma", + avatar_url: "https://avatars.githubusercontent.com/u/3165839?v=4", + latency_ms: { + p50: 82.97, + p95: 147.3868, + }, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "b006209d-fdd2-4716-afb2-104dafb32dfb", + username: "mtojek", + avatar_url: "https://avatars.githubusercontent.com/u/14044910?v=4", + latency_ms: { + p50: 36.758, + p95: 101.31679999999983, + }, + }, + ], + }, + }, + }, +} diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 02333d2d5ded0..03f690853e73d 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -18,20 +18,28 @@ import { colors } from "theme/colors" import { Helmet } from "react-helmet-async" import { getTemplatePageTitle } from "../utils" import { Loader } from "components/Loader/Loader" -import { DAUsResponse, TemplateInsightsResponse } from "api/typesGenerated" +import { + DAUsResponse, + TemplateInsightsResponse, + UserLatencyInsightsResponse, +} from "api/typesGenerated" import { ComponentProps } from "react" import subDays from "date-fns/subDays" export default function TemplateInsightsPage() { const { template } = useTemplateLayoutContext() + const insightsFilter = { + template_ids: template.id, + start_time: toTimeFilter(sevenDaysAgo()), + end_time: toTimeFilter(new Date()), + } const { data: templateInsights } = useQuery({ queryKey: ["templates", template.id, "usage"], - queryFn: () => - getInsightsTemplate({ - template_ids: template.id, - start_time: toTimeFilter(sevenDaysAgo()), - end_time: toTimeFilter(new Date()), - }), + queryFn: () => getInsightsTemplate(insightsFilter), + }) + const { data: userLatency } = useQuery({ + queryKey: ["templates", template.id, "user-latency"], + queryFn: () => getInsightsUserLatency(insightsFilter), }) return ( @@ -39,29 +47,43 @@ export default function TemplateInsightsPage() { {getTemplatePageTitle("Insights", template)} - - theme.spacing(3), - }} - > - - - - + ) } +export const TemplateInsightsPageView = ({ + templateInsights, + userLatency, +}: { + templateInsights: TemplateInsightsResponse | undefined + userLatency: UserLatencyInsightsResponse | undefined +}) => { + return ( + theme.spacing(3), + }} + > + + + + + ) +} + const DailyUsersPanel = ({ data, ...panelProps @@ -91,22 +113,15 @@ const DailyUsersPanel = ({ ) } -const UserLatencyPanel = (props: PanelProps) => { - const { template } = useTemplateLayoutContext() - const { data } = useQuery({ - queryKey: ["templates", template.id, "user-latency"], - queryFn: () => - getInsightsUserLatency({ - template_ids: template.id, - start_time: toTimeFilter(sevenDaysAgo()), - end_time: toTimeFilter(new Date()), - }), - }) +const UserLatencyPanel = ({ + data, + ...panelProps +}: PanelProps & { data: UserLatencyInsightsResponse | undefined }) => { const theme = useTheme() const users = data?.report.users return ( - + Latency by user From 3bc6e6b6148b40bc7c7f8dd4de00441e297399da Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 26 Jul 2023 12:56:21 +0000 Subject: [PATCH 24/24] Fix deploy settings page --- site/src/components/DAUChart/DAUChart.tsx | 20 +++++++++++++++++++ .../GeneralSettingsPageView.tsx | 12 +++++++++-- .../TemplateInsightsPage.tsx | 12 +++-------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/site/src/components/DAUChart/DAUChart.tsx b/site/src/components/DAUChart/DAUChart.tsx index b3fecf965808b..c08cf5bc1db2f 100644 --- a/site/src/components/DAUChart/DAUChart.tsx +++ b/site/src/components/DAUChart/DAUChart.tsx @@ -1,3 +1,4 @@ +import Box from "@mui/material/Box" import { Theme } from "@mui/material/styles" import useTheme from "@mui/styles/useTheme" import * as TypesGen from "api/typesGenerated" @@ -15,6 +16,11 @@ import { Tooltip, } from "chart.js" import "chartjs-adapter-date-fns" +import { + HelpTooltip, + HelpTooltipTitle, + HelpTooltipText, +} from "components/Tooltips/HelpTooltip" import dayjs from "dayjs" import { FC } from "react" import { Line } from "react-chartjs-2" @@ -93,3 +99,17 @@ export const DAUChart: FC = ({ daus }) => { /> ) } + +export const DAUTitle = () => { + return ( + + Daily Active Users + + How do we calculate DAUs? + + We use all workspace connection traffic to calculate DAUs. + + + + ) +} diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx index 7bc000e96d84b..8e9df03477f0f 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx @@ -1,10 +1,12 @@ +import Box from "@mui/material/Box" import { DeploymentOption } from "api/types" import { DAUsResponse } from "api/typesGenerated" import { ErrorAlert } from "components/Alert/ErrorAlert" -import { DAUChart } from "components/DAUChart/DAUChart" +import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart" import { Header } from "components/DeploySettingsLayout/Header" import OptionsTable from "components/DeploySettingsLayout/OptionsTable" import { Stack } from "components/Stack/Stack" +import { WorkspaceSection } from "components/WorkspaceSection/WorkspaceSection" import { useDeploymentOptions } from "utils/deployOptions" import { docs } from "utils/docs" @@ -29,7 +31,13 @@ export const GeneralSettingsPageView = ({ {Boolean(getDeploymentDAUsError) && ( )} - {deploymentDAUs && } + {deploymentDAUs && ( + + }> + + + + )} - - Daily Active Users - - How do we calculate DAUs? - - We use all workspace connection traffic to calculate DAUs. - - + + Last 7 days