mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): Add deployment-wide DAU chart (#5810)
This commit is contained in:
@@ -644,6 +644,12 @@ export const getTemplateDAUs = async (
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getDeploymentDAUs =
|
||||
async (): Promise<TypesGen.DeploymentDAUsResponse> => {
|
||||
const response = await axios.get(`/api/v2/insights/daus`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getTemplateACL = async (
|
||||
templateId: string,
|
||||
): Promise<TypesGen.TemplateACL> => {
|
||||
|
||||
@@ -342,6 +342,11 @@ export interface DeploymentConfigField<T extends Flaggable> {
|
||||
readonly value: T
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface DeploymentDAUsResponse {
|
||||
readonly entries: DAUEntry[]
|
||||
}
|
||||
|
||||
// From codersdk/features.go
|
||||
export interface Entitlements {
|
||||
readonly features: Record<FeatureName, Feature>
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ describe("DAUChart", () => {
|
||||
it("renders a helpful paragraph on empty state", async () => {
|
||||
render(
|
||||
<DAUChart
|
||||
templateDAUs={{
|
||||
daus={{
|
||||
entries: [],
|
||||
}}
|
||||
/>,
|
||||
@@ -24,7 +24,7 @@ describe("DAUChart", () => {
|
||||
it("renders a graph", async () => {
|
||||
render(
|
||||
<DAUChart
|
||||
templateDAUs={{
|
||||
daus={{
|
||||
entries: [{ date: "2020-01-01", amount: 1 }],
|
||||
}}
|
||||
/>,
|
||||
+5
-7
@@ -38,19 +38,17 @@ ChartJS.register(
|
||||
)
|
||||
|
||||
export interface DAUChartProps {
|
||||
templateDAUs: TypesGen.TemplateDAUsResponse
|
||||
daus: TypesGen.TemplateDAUsResponse | TypesGen.DeploymentDAUsResponse
|
||||
}
|
||||
export const Language = {
|
||||
loadingText: "DAU stats are loading. Check back later.",
|
||||
chartTitle: "Daily Active Users",
|
||||
}
|
||||
|
||||
export const DAUChart: FC<DAUChartProps> = ({
|
||||
templateDAUs: templateMetricsData,
|
||||
}) => {
|
||||
export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||
const theme: Theme = useTheme()
|
||||
|
||||
if (templateMetricsData.entries.length === 0) {
|
||||
if (daus.entries.length === 0) {
|
||||
return (
|
||||
// We generate hidden element to prove this path is taken in the test
|
||||
// and through site inspection.
|
||||
@@ -60,11 +58,11 @@ export const DAUChart: FC<DAUChartProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const labels = templateMetricsData.entries.map((val) => {
|
||||
const labels = daus.entries.map((val) => {
|
||||
return dayjs(val.date).format("YYYY-MM-DD")
|
||||
})
|
||||
|
||||
const data = templateMetricsData.entries.map((val) => {
|
||||
const data = daus.entries.map((val) => {
|
||||
return val.amount
|
||||
})
|
||||
|
||||
@@ -5,13 +5,18 @@ import { Sidebar } from "./Sidebar"
|
||||
import { createContext, Suspense, useContext, FC } from "react"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { DeploymentConfig } from "api/typesGenerated"
|
||||
import { DeploymentConfig, DeploymentDAUsResponse } from "api/typesGenerated"
|
||||
import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine"
|
||||
import { RequirePermission } from "components/RequirePermission/RequirePermission"
|
||||
import { usePermissions } from "hooks/usePermissions"
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
type DeploySettingsContextValue = { deploymentConfig: DeploymentConfig }
|
||||
type DeploySettingsContextValue = {
|
||||
deploymentConfig: DeploymentConfig
|
||||
getDeploymentConfigError: unknown
|
||||
deploymentDAUs?: DeploymentDAUsResponse
|
||||
getDeploymentDAUsError: unknown
|
||||
}
|
||||
|
||||
const DeploySettingsContext = createContext<
|
||||
DeploySettingsContextValue | undefined
|
||||
@@ -30,7 +35,12 @@ export const useDeploySettings = (): DeploySettingsContextValue => {
|
||||
export const DeploySettingsLayout: FC = () => {
|
||||
const [state] = useMachine(deploymentConfigMachine)
|
||||
const styles = useStyles()
|
||||
const { deploymentConfig } = state.context
|
||||
const {
|
||||
deploymentConfig,
|
||||
deploymentDAUs,
|
||||
getDeploymentConfigError,
|
||||
getDeploymentDAUsError,
|
||||
} = state.context
|
||||
const permissions = usePermissions()
|
||||
|
||||
return (
|
||||
@@ -41,7 +51,12 @@ export const DeploySettingsLayout: FC = () => {
|
||||
<main className={styles.content}>
|
||||
{deploymentConfig ? (
|
||||
<DeploySettingsContext.Provider
|
||||
value={{ deploymentConfig: deploymentConfig }}
|
||||
value={{
|
||||
deploymentConfig,
|
||||
getDeploymentConfigError,
|
||||
deploymentDAUs,
|
||||
getDeploymentDAUsError,
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
|
||||
@@ -5,14 +5,19 @@ import { pageTitle } from "util/page"
|
||||
import { GeneralSettingsPageView } from "./GeneralSettingsPageView"
|
||||
|
||||
const GeneralSettingsPage: FC = () => {
|
||||
const { deploymentConfig: deploymentConfig } = useDeploySettings()
|
||||
const { deploymentConfig, deploymentDAUs, getDeploymentDAUsError } =
|
||||
useDeploySettings()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("General Settings")}</title>
|
||||
</Helmet>
|
||||
<GeneralSettingsPageView deploymentConfig={deploymentConfig} />
|
||||
<GeneralSettingsPageView
|
||||
deploymentConfig={deploymentConfig}
|
||||
deploymentDAUs={deploymentDAUs}
|
||||
getDeploymentDAUsError={getDeploymentDAUsError}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+18
@@ -1,4 +1,8 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
MockDeploymentDAUResponse,
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
GeneralSettingsPageView,
|
||||
GeneralSettingsPageViewProps,
|
||||
@@ -24,6 +28,9 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
deploymentDAUs: {
|
||||
defaultValue: MockDeploymentDAUResponse,
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof GeneralSettingsPageView>
|
||||
|
||||
@@ -31,3 +38,14 @@ const Template: Story<GeneralSettingsPageViewProps> = (args) => (
|
||||
<GeneralSettingsPageView {...args} />
|
||||
)
|
||||
export const Page = Template.bind({})
|
||||
|
||||
export const NoDAUs = Template.bind({})
|
||||
NoDAUs.args = {
|
||||
deploymentDAUs: undefined,
|
||||
}
|
||||
|
||||
export const DAUError = Template.bind({})
|
||||
DAUError.args = {
|
||||
deploymentDAUs: undefined,
|
||||
getDeploymentDAUsError: makeMockApiError({ message: "Error fetching DAUs." }),
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { DeploymentConfig } from "api/typesGenerated"
|
||||
import { DeploymentConfig, DeploymentDAUsResponse } from "api/typesGenerated"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { DAUChart } from "components/DAUChart/DAUChart"
|
||||
import { Header } from "components/DeploySettingsLayout/Header"
|
||||
import OptionsTable from "components/DeploySettingsLayout/OptionsTable"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
|
||||
export type GeneralSettingsPageViewProps = {
|
||||
deploymentConfig: Pick<DeploymentConfig, "access_url" | "wildcard_access_url">
|
||||
deploymentDAUs?: DeploymentDAUsResponse
|
||||
getDeploymentDAUsError: unknown
|
||||
}
|
||||
export const GeneralSettingsPageView = ({
|
||||
deploymentConfig,
|
||||
deploymentDAUs,
|
||||
getDeploymentDAUsError,
|
||||
}: GeneralSettingsPageViewProps): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
@@ -15,12 +22,18 @@ export const GeneralSettingsPageView = ({
|
||||
description="Information about your Coder deployment."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/configure"
|
||||
/>
|
||||
<OptionsTable
|
||||
options={{
|
||||
access_url: deploymentConfig.access_url,
|
||||
wildcard_access_url: deploymentConfig.wildcard_access_url,
|
||||
}}
|
||||
/>
|
||||
<Stack spacing={4}>
|
||||
{Boolean(getDeploymentDAUsError) && (
|
||||
<AlertBanner error={getDeploymentDAUsError} severity="error" />
|
||||
)}
|
||||
{deploymentDAUs && <DAUChart daus={deploymentDAUs} />}
|
||||
<OptionsTable
|
||||
options={{
|
||||
access_url: deploymentConfig.access_url,
|
||||
wildcard_access_url: deploymentConfig.wildcard_access_url,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TemplateStats } from "components/TemplateStats/TemplateStats"
|
||||
import { VersionsTable } from "components/VersionsTable/VersionsTable"
|
||||
import frontMatter from "front-matter"
|
||||
import { FC } from "react"
|
||||
import { DAUChart } from "./DAUChart"
|
||||
import { DAUChart } from "../../../components/DAUChart/DAUChart"
|
||||
|
||||
export interface TemplateSummaryPageViewProps {
|
||||
template: Template
|
||||
@@ -46,7 +46,7 @@ export const TemplateSummaryPageView: FC<
|
||||
template={template}
|
||||
activeVersion={activeTemplateVersion}
|
||||
/>
|
||||
{templateDAUs && <DAUChart templateDAUs={templateDAUs} />}
|
||||
{templateDAUs && <DAUChart daus={templateDAUs} />}
|
||||
<TemplateResourcesTable
|
||||
resources={getStartedResources(templateResources)}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,13 @@ export const MockTemplateDAUResponse: TypesGen.TemplateDAUsResponse = {
|
||||
{ date: "2022-08-30T00:00:00Z", amount: 1 },
|
||||
],
|
||||
}
|
||||
export const MockDeploymentDAUResponse: TypesGen.DeploymentDAUsResponse = {
|
||||
entries: [
|
||||
{ date: "2022-08-27T00:00:00Z", amount: 1 },
|
||||
{ date: "2022-08-29T00:00:00Z", amount: 2 },
|
||||
{ date: "2022-08-30T00:00:00Z", amount: 1 },
|
||||
],
|
||||
}
|
||||
export const MockSessionToken: TypesGen.LoginWithPasswordResponse = {
|
||||
session_token: "my-session-token",
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ export const handlers = [
|
||||
return res(ctx.status(200), ctx.json(M.MockTemplateDAUResponse))
|
||||
}),
|
||||
|
||||
rest.get("/api/v2/insights/daus", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockDeploymentDAUResponse))
|
||||
}),
|
||||
|
||||
// build info
|
||||
rest.get("/api/v2/buildinfo", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockBuildInfo))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getDeploymentConfig } from "api/api"
|
||||
import { DeploymentConfig } from "api/typesGenerated"
|
||||
import { getDeploymentConfig, getDeploymentDAUs } from "api/api"
|
||||
import { DeploymentConfig, DeploymentDAUsResponse } from "api/typesGenerated"
|
||||
import { createMachine, assign } from "xstate"
|
||||
|
||||
export const deploymentConfigMachine = createMachine(
|
||||
@@ -11,29 +11,49 @@ export const deploymentConfigMachine = createMachine(
|
||||
context: {} as {
|
||||
deploymentConfig?: DeploymentConfig
|
||||
getDeploymentConfigError?: unknown
|
||||
deploymentDAUs?: DeploymentDAUsResponse
|
||||
getDeploymentDAUsError?: unknown
|
||||
},
|
||||
events: {} as { type: "LOAD" },
|
||||
services: {} as {
|
||||
getDeploymentConfig: {
|
||||
data: DeploymentConfig
|
||||
}
|
||||
getDeploymentDAUs: {
|
||||
data: DeploymentDAUsResponse
|
||||
}
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import("./deploymentConfigMachine.typegen").Typegen0,
|
||||
initial: "loading",
|
||||
initial: "config",
|
||||
states: {
|
||||
loading: {
|
||||
config: {
|
||||
invoke: {
|
||||
src: "getDeploymentConfig",
|
||||
onDone: {
|
||||
target: "done",
|
||||
target: "daus",
|
||||
actions: ["assignDeploymentConfig"],
|
||||
},
|
||||
onError: {
|
||||
target: "done",
|
||||
target: "daus",
|
||||
actions: ["assignGetDeploymentConfigError"],
|
||||
},
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
daus: {
|
||||
invoke: {
|
||||
src: "getDeploymentDAUs",
|
||||
onDone: {
|
||||
target: "done",
|
||||
actions: ["assignDeploymentDAUs"],
|
||||
},
|
||||
onError: {
|
||||
target: "done",
|
||||
actions: ["assignGetDeploymentDAUsError"],
|
||||
},
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
done: {
|
||||
type: "final",
|
||||
@@ -43,6 +63,7 @@ export const deploymentConfigMachine = createMachine(
|
||||
{
|
||||
services: {
|
||||
getDeploymentConfig: getDeploymentConfig,
|
||||
getDeploymentDAUs: getDeploymentDAUs,
|
||||
},
|
||||
actions: {
|
||||
assignDeploymentConfig: assign({
|
||||
@@ -51,6 +72,12 @@ export const deploymentConfigMachine = createMachine(
|
||||
assignGetDeploymentConfigError: assign({
|
||||
getDeploymentConfigError: (_, { data }) => data,
|
||||
}),
|
||||
assignDeploymentDAUs: assign({
|
||||
deploymentDAUs: (_, { data }) => data,
|
||||
}),
|
||||
assignGetDeploymentDAUsError: assign({
|
||||
getDeploymentDAUsError: (_, { data }) => data,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user