mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add template insights page (#8722)
This commit is contained in:
Generated
+4
-2
@@ -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": {
|
||||
|
||||
Generated
+4
-2
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+1
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = () => {
|
||||
<Route path="files" element={<TemplateFilesPage />} />
|
||||
<Route path="versions" element={<TemplateVersionsPage />} />
|
||||
<Route path="embed" element={<TemplateEmbedPage />} />
|
||||
<Route path="insights" element={<TemplateInsightsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="workspace" element={<CreateWorkspacePage />} />
|
||||
|
||||
@@ -1368,3 +1368,28 @@ export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => {
|
||||
buildParameters,
|
||||
}
|
||||
}
|
||||
|
||||
type InsightsFilter = {
|
||||
start_time: string
|
||||
end_time: string
|
||||
template_ids: string
|
||||
}
|
||||
|
||||
export const getInsightsUserLatency = async (
|
||||
filters: InsightsFilter,
|
||||
): Promise<TypesGen.UserLatencyInsightsResponse> => {
|
||||
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<TypesGen.TemplateInsightsResponse> => {
|
||||
const params = new URLSearchParams({
|
||||
...filters,
|
||||
interval: "day",
|
||||
})
|
||||
const response = await axios.get(`/api/v2/insights/templates?${params}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -1555,6 +1555,7 @@ export type Experiment =
|
||||
| "moons"
|
||||
| "single_tailnet"
|
||||
| "tailnet_ha_coordinator"
|
||||
| "template_insights_page"
|
||||
| "template_restart_requirement"
|
||||
| "workspace_actions"
|
||||
export const Experiments: Experiment[] = [
|
||||
@@ -1562,6 +1563,7 @@ export const Experiments: Experiment[] = [
|
||||
"moons",
|
||||
"single_tailnet",
|
||||
"tailnet_ha_coordinator",
|
||||
"template_insights_page",
|
||||
"template_restart_requirement",
|
||||
"workspace_actions",
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
<DAUChart
|
||||
daus={{
|
||||
tz_hour_offset: 0,
|
||||
entries: [],
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
await screen.findAllByText(Language.loadingText)
|
||||
})
|
||||
it("renders a graph", async () => {
|
||||
render(
|
||||
<DAUChart
|
||||
daus={{
|
||||
tz_hour_offset: 0,
|
||||
entries: [{ date: "2020-01-01", amount: 1 }],
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
await screen.findAllByText(Language.chartTitle)
|
||||
})
|
||||
})
|
||||
@@ -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,13 +16,11 @@ import {
|
||||
Tooltip,
|
||||
} from "chart.js"
|
||||
import "chartjs-adapter-date-fns"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
HelpTooltipText,
|
||||
} 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 +39,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<DAUChartProps> = ({ 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.
|
||||
<div style={{ display: "none" }}>
|
||||
<p>{Language.loadingText}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const labels = daus.entries.map((val) => {
|
||||
return dayjs(val.date).format("YYYY-MM-DD")
|
||||
})
|
||||
@@ -92,42 +77,39 @@ export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
aspectRatio: 10 / 1,
|
||||
maintainAspectRatio: false,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkspaceSection
|
||||
title={
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
{Language.chartTitle}
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>How do we calculate DAUs?</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
We use all workspace connection traffic to calculate DAUs.
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Line
|
||||
data-chromatic="ignore"
|
||||
data={{
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Daily Active Users",
|
||||
data: data,
|
||||
tension: 1 / 4,
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
borderColor: theme.palette.secondary.dark,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={options}
|
||||
height={400}
|
||||
/>
|
||||
</WorkspaceSection>
|
||||
</>
|
||||
<Line
|
||||
data-chromatic="ignore"
|
||||
data={{
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Daily Active Users",
|
||||
data: data,
|
||||
tension: 1 / 4,
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
borderColor: theme.palette.secondary.dark,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const DAUTitle = () => {
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
Daily Active Users
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>How do we calculate DAUs?</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
We use all workspace connection traffic to calculate DAUs.
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<React.PropsWithChildren<{ size?: number }>> = ({
|
||||
export const Loader: FC<{ size?: number } & BoxProps> = ({
|
||||
size = 26,
|
||||
...boxProps
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
@@ -13,6 +14,7 @@ export const Loader: FC<React.PropsWithChildren<{ size?: number }>> = ({
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
data-testid="loader"
|
||||
{...boxProps}
|
||||
>
|
||||
<CircularProgress size={size} />
|
||||
</Box>
|
||||
|
||||
@@ -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,6 +164,19 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
||||
>
|
||||
Embed
|
||||
</NavLink>
|
||||
{shouldShowInsights && (
|
||||
<NavLink
|
||||
to={`/templates/${templateName}/insights`}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.tabItem,
|
||||
isActive ? styles.tabItemActive : undefined,
|
||||
])
|
||||
}
|
||||
>
|
||||
Insights
|
||||
</NavLink>
|
||||
)}
|
||||
</Stack>
|
||||
</Margins>
|
||||
</div>
|
||||
|
||||
@@ -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) && (
|
||||
<ErrorAlert error={getDeploymentDAUsError} />
|
||||
)}
|
||||
{deploymentDAUs && <DAUChart daus={deploymentDAUs} />}
|
||||
{deploymentDAUs && (
|
||||
<Box height={200} sx={{ mb: 3 }}>
|
||||
<WorkspaceSection title={<DAUTitle />}>
|
||||
<DAUChart daus={deploymentDAUs} />
|
||||
</WorkspaceSection>
|
||||
</Box>
|
||||
)}
|
||||
<OptionsTable
|
||||
options={useDeploymentOptions(
|
||||
deploymentOptions,
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import { TemplateInsightsPageView } from "./TemplateInsightsPage"
|
||||
|
||||
const meta: Meta<typeof TemplateInsightsPageView> = {
|
||||
title: "pages/TemplateInsightsPageView",
|
||||
component: TemplateInsightsPageView,
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof TemplateInsightsPageView>
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
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 { getInsightsTemplate, getInsightsUserLatency } from "api/api"
|
||||
import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart"
|
||||
import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipTitle,
|
||||
HelpTooltipText,
|
||||
} 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"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { getTemplatePageTitle } from "../utils"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
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(insightsFilter),
|
||||
})
|
||||
const { data: userLatency } = useQuery({
|
||||
queryKey: ["templates", template.id, "user-latency"],
|
||||
queryFn: () => getInsightsUserLatency(insightsFilter),
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{getTemplatePageTitle("Insights", template)}</title>
|
||||
</Helmet>
|
||||
<TemplateInsightsPageView
|
||||
templateInsights={templateInsights}
|
||||
userLatency={userLatency}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const TemplateInsightsPageView = ({
|
||||
templateInsights,
|
||||
userLatency,
|
||||
}: {
|
||||
templateInsights: TemplateInsightsResponse | undefined
|
||||
userLatency: UserLatencyInsightsResponse | undefined
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
||||
gridTemplateRows: "440px auto",
|
||||
gap: (theme) => theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
<DailyUsersPanel
|
||||
sx={{ gridColumn: "span 2" }}
|
||||
data={templateInsights?.interval_reports}
|
||||
/>
|
||||
<UserLatencyPanel data={userLatency} />
|
||||
<TemplateUsagePanel
|
||||
sx={{ gridColumn: "span 3" }}
|
||||
data={templateInsights?.report.apps_usage}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const DailyUsersPanel = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateInsightsResponse["interval_reports"] | undefined
|
||||
}) => {
|
||||
return (
|
||||
<Panel {...panelProps}>
|
||||
<PanelHeader>
|
||||
<PanelTitle>
|
||||
<DAUTitle />
|
||||
</PanelTitle>
|
||||
<PanelSubtitle>Last 7 days</PanelSubtitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: "100%" }} />}
|
||||
{data && data.length === 0 && <NoDataAvailable />}
|
||||
{data && data.length > 0 && <DAUChart daus={mapToDAUsResponse(data)} />}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
const UserLatencyPanel = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & { data: UserLatencyInsightsResponse | undefined }) => {
|
||||
const theme = useTheme()
|
||||
const users = data?.report.users
|
||||
|
||||
return (
|
||||
<Panel {...panelProps} sx={{ overflowY: "auto", ...panelProps.sx }}>
|
||||
<PanelHeader>
|
||||
<PanelTitle sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
Latency by user
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>How do we calculate latency?</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
The average latency of user connections to workspaces.
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
</PanelTitle>
|
||||
<PanelSubtitle>Last 7 days</PanelSubtitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: "100%" }} />}
|
||||
{users && users.length === 0 && <NoDataAvailable />}
|
||||
{users &&
|
||||
users
|
||||
.sort((a, b) => b.latency_ms.p95 - a.latency_ms.p95)
|
||||
.map((row) => (
|
||||
<Box
|
||||
key={row.user_id}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: 14,
|
||||
py: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
||||
<UserAvatar
|
||||
username={row.username}
|
||||
avatarURL={row.avatar_url}
|
||||
/>
|
||||
<Box sx={{ fontWeight: 500 }}>{row.username}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
color: getLatencyColor(theme, row.latency_ms.p95),
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{row.latency_ms.p95.toFixed(0)}ms
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
const TemplateUsagePanel = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateInsightsResponse["report"]["apps_usage"] | undefined
|
||||
}) => {
|
||||
const validUsage = data?.filter((u) => u.seconds > 0)
|
||||
const totalInSeconds =
|
||||
validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1
|
||||
const usageColors = chroma
|
||||
.scale([colors.green[8], colors.blue[8]])
|
||||
.mode("lch")
|
||||
.colors(validUsage?.length ?? 0)
|
||||
// The API returns a row for each app, even if the user didn't use it.
|
||||
const hasDataAvailable = validUsage && validUsage.length > 0
|
||||
return (
|
||||
<Panel {...panelProps}>
|
||||
<PanelHeader>
|
||||
<PanelTitle>App‘s & IDE usage</PanelTitle>
|
||||
<PanelSubtitle>Last 7 days</PanelSubtitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: 200 }} />}
|
||||
{data && !hasDataAvailable && <NoDataAvailable sx={{ height: 200 }} />}
|
||||
{data && hasDataAvailable && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||
{validUsage
|
||||
.sort((a, b) => b.seconds - a.seconds)
|
||||
.map((usage, i) => {
|
||||
const percentage = (usage.seconds / totalInSeconds) * 100
|
||||
return (
|
||||
<Box
|
||||
key={usage.slug}
|
||||
sx={{ display: "flex", gap: 2, alignItems: "center" }}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={usage.icon}
|
||||
alt=""
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ fontSize: 13, fontWeight: 500, width: 200 }}>
|
||||
{usage.display_name}
|
||||
</Box>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
value={percentage}
|
||||
variant="determinate"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 8,
|
||||
backgroundColor: (theme) => theme.palette.divider,
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: usageColors[i],
|
||||
borderRadius: 999,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
width: 200,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{formatTime(usage.seconds)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
const Panel = styled(Box)(({ theme }) => ({
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}))
|
||||
|
||||
type PanelProps = ComponentProps<typeof Panel>
|
||||
|
||||
const PanelHeader = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(2.5, 3, 3),
|
||||
}))
|
||||
|
||||
const PanelTitle = styled(Box)(() => ({
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
}))
|
||||
|
||||
const PanelSubtitle = styled(Box)(({ theme }) => ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
}))
|
||||
|
||||
const PanelContent = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(0, 3, 3),
|
||||
flex: 1,
|
||||
}))
|
||||
|
||||
const NoDataAvailable = (props: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
{...props}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
No data available
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0")
|
||||
const day = String(date.getUTCDate()).padStart(2, "0")
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sevenDaysAgo() {
|
||||
return subDays(new Date(), 7)
|
||||
}
|
||||
@@ -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 = () => {
|
||||
<title>{getTemplatePageTitle("Template", template)}</title>
|
||||
</Helmet>
|
||||
<TemplateSummaryPageView
|
||||
data={data}
|
||||
resources={resources}
|
||||
template={template}
|
||||
activeVersion={activeVersion}
|
||||
/>
|
||||
|
||||
+34
-46
@@ -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<typeof TemplateSummaryPageView> = {
|
||||
title: "pages/TemplateSummaryPageView",
|
||||
component: TemplateSummaryPageView,
|
||||
}
|
||||
|
||||
const Template: Story<TemplateSummaryPageViewProps> = (args) => (
|
||||
<TemplateSummaryPageView {...args} />
|
||||
)
|
||||
export default meta
|
||||
type Story = StoryObj<typeof TemplateSummaryPageView>
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<TemplateSummaryPageViewProps> = ({
|
||||
data,
|
||||
resources,
|
||||
template,
|
||||
activeVersion,
|
||||
}) => {
|
||||
@@ -35,12 +33,10 @@ export const TemplateSummaryPageView: FC<TemplateSummaryPageViewProps> = ({
|
||||
}
|
||||
}, [template, navigate, location])
|
||||
|
||||
if (!data) {
|
||||
if (!resources) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
const { daus, resources } = data
|
||||
|
||||
const getStartedResources = (resources: WorkspaceResource[]) => {
|
||||
return resources.filter(
|
||||
(resource) => resource.workspace_transition === "start",
|
||||
@@ -51,7 +47,6 @@ export const TemplateSummaryPageView: FC<TemplateSummaryPageViewProps> = ({
|
||||
<Stack spacing={4}>
|
||||
<TemplateVersionWarnings warnings={activeVersion.warnings} />
|
||||
<TemplateStats template={template} activeVersion={activeVersion} />
|
||||
{daus && <DAUChart daus={daus} />}
|
||||
<TemplateResourcesTable resources={getStartedResources(resources)} />
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -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<typeof fetchTemplateSummary>
|
||||
>
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user