feat(site): Add deployment-wide DAU chart (#5810)

This commit is contained in:
Presley Pizzo 2023-01-25 20:03:47 -05:00 committed by GitHub
parent e7b8318b87
commit 16d8cc4176
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 568 additions and 31 deletions

36
coderd/apidoc/docs.go generated
View File

@ -648,6 +648,31 @@ const docTemplate = `{
}
}
},
"/insights/daus": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Insights"
],
"summary": "Get deployment DAUs",
"operationId": "get-deployment-daus",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.DeploymentDAUsResponse"
}
}
}
}
},
"/licenses": {
"get": {
"security": [
@ -6149,6 +6174,17 @@ const docTemplate = `{
}
}
},
"codersdk.DeploymentDAUsResponse": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.DAUEntry"
}
}
}
},
"codersdk.Entitlement": {
"type": "string",
"enum": [

View File

@ -558,6 +558,27 @@
}
}
},
"/insights/daus": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Insights"],
"summary": "Get deployment DAUs",
"operationId": "get-deployment-daus",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.DeploymentDAUsResponse"
}
}
}
}
},
"/licenses": {
"get": {
"security": [
@ -5486,6 +5507,17 @@
}
}
},
"codersdk.DeploymentDAUsResponse": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.DAUEntry"
}
}
}
},
"codersdk.Entitlement": {
"type": "string",
"enum": ["entitled", "grace_period", "not_entitled"],

View File

@ -621,7 +621,10 @@ func New(options *Options) *API {
r.Get("/", api.workspaceApplicationAuth)
})
})
r.Route("/insights", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/daus", api.deploymentDAUs)
})
r.Route("/debug", func(r chi.Router) {
r.Use(
apiKeyMiddleware,

View File

@ -323,6 +323,42 @@ func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, templateID uuid.UUID) (
return rs, nil
}
func (q *fakeQuerier) GetDeploymentDAUs(_ context.Context) ([]database.GetDeploymentDAUsRow, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
seens := make(map[time.Time]map[uuid.UUID]struct{})
for _, as := range q.agentStats {
date := as.CreatedAt.Truncate(time.Hour * 24)
dateEntry := seens[date]
if dateEntry == nil {
dateEntry = make(map[uuid.UUID]struct{})
}
dateEntry[as.UserID] = struct{}{}
seens[date] = dateEntry
}
seenKeys := maps.Keys(seens)
sort.Slice(seenKeys, func(i, j int) bool {
return seenKeys[i].Before(seenKeys[j])
})
var rs []database.GetDeploymentDAUsRow
for _, key := range seenKeys {
ids := seens[key]
for id := range ids {
rs = append(rs, database.GetDeploymentDAUsRow{
Date: key,
UserID: id,
})
}
}
return rs, nil
}
func (q *fakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
if err := validateDatabaseType(arg); err != nil {
return database.GetTemplateAverageBuildTimeRow{}, err

View File

@ -40,6 +40,7 @@ type sqlcQuerier interface {
// are included.
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
GetDERPMeshKey(ctx context.Context) (string, error)
GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUsRow, error)
GetDeploymentID(ctx context.Context) (string, error)
GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error)
GetFileByID(ctx context.Context, id uuid.UUID) (File, error)

View File

@ -25,6 +25,46 @@ func (q *sqlQuerier) DeleteOldAgentStats(ctx context.Context) error {
return err
}
const getDeploymentDAUs = `-- name: GetDeploymentDAUs :many
SELECT
(created_at at TIME ZONE 'UTC')::date as date,
user_id
FROM
agent_stats
GROUP BY
date, user_id
ORDER BY
date ASC
`
type GetDeploymentDAUsRow struct {
Date time.Time `db:"date" json:"date"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUsRow, error) {
rows, err := q.db.QueryContext(ctx, getDeploymentDAUs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetDeploymentDAUsRow
for rows.Next() {
var i GetDeploymentDAUsRow
if err := rows.Scan(&i.Date, &i.UserID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateDAUs = `-- name: GetTemplateDAUs :many
SELECT
(created_at at TIME ZONE 'UTC')::date as date,

View File

@ -25,5 +25,16 @@ GROUP BY
ORDER BY
date ASC;
-- name: GetDeploymentDAUs :many
SELECT
(created_at at TIME ZONE 'UTC')::date as date,
user_id
FROM
agent_stats
GROUP BY
date, user_id
ORDER BY
date ASC;
-- name: DeleteOldAgentStats :exec
DELETE FROM agent_stats WHERE created_at < NOW() - INTERVAL '30 days';

33
coderd/insights.go Normal file
View File

@ -0,0 +1,33 @@
package coderd
import (
"net/http"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
)
// @Summary Get deployment DAUs
// @ID get-deployment-daus
// @Security CoderSessionToken
// @Produce json
// @Tags Insights
// @Success 200 {object} codersdk.DeploymentDAUsResponse
// @Router /insights/daus [get]
func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentConfig) {
httpapi.Forbidden(rw)
return
}
resp, _ := api.metricsCache.DeploymentDAUs()
if resp == nil || resp.Entries == nil {
httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DeploymentDAUsResponse{
Entries: []codersdk.DAUEntry{},
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, resp)
}

122
coderd/insights_test.go Normal file
View File

@ -0,0 +1,122 @@
package coderd_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/testutil"
)
func TestDeploymentInsights(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Millisecond * 100,
MetricsCacheRefreshInterval: time.Millisecond * 100,
})
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: authToken,
},
}},
}},
},
},
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SetSessionToken(authToken)
agentCloser := agent.New(agent.Options{
Logger: slogtest.Make(t, nil),
Client: agentClient,
})
defer func() {
_ = agentCloser.Close()
}()
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
daus, err := client.DeploymentDAUs(context.Background())
require.NoError(t, err)
require.Equal(t, &codersdk.DeploymentDAUsResponse{
Entries: []codersdk.DAUEntry{},
}, daus, "no DAUs when stats are empty")
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
assert.Zero(t, res.Workspaces[0].LastUsedAt)
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{
Logger: slogtest.Make(t, nil).Named("tailnet"),
})
require.NoError(t, err)
defer func() {
_ = conn.Close()
}()
sshConn, err := conn.SSHClient(ctx)
require.NoError(t, err)
_ = sshConn.Close()
wantDAUs := &codersdk.DeploymentDAUsResponse{
Entries: []codersdk.DAUEntry{
{
Date: time.Now().UTC().Truncate(time.Hour * 24),
Amount: 1,
},
},
}
require.Eventuallyf(t, func() bool {
daus, err = client.DeploymentDAUs(ctx)
require.NoError(t, err)
return len(daus.Entries) > 0
},
testutil.WaitShort, testutil.IntervalFast,
"deployment daus never loaded",
)
gotDAUs, err := client.DeploymentDAUs(ctx)
require.NoError(t, err)
require.Equal(t, gotDAUs, wantDAUs)
template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
}

View File

@ -27,6 +27,7 @@ type Cache struct {
database database.Store
log slog.Logger
deploymentDAUResponses atomic.Pointer[codersdk.DeploymentDAUsResponse]
templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse]
templateUniqueUsers atomic.Pointer[map[uuid.UUID]int]
templateAverageBuildTime atomic.Pointer[map[uuid.UUID]database.GetTemplateAverageBuildTimeRow]
@ -110,6 +111,28 @@ func convertDAUResponse(rows []database.GetTemplateDAUsRow) codersdk.TemplateDAU
return resp
}
func convertDeploymentDAUResponse(rows []database.GetDeploymentDAUsRow) codersdk.DeploymentDAUsResponse {
respMap := make(map[time.Time][]uuid.UUID)
for _, row := range rows {
respMap[row.Date] = append(respMap[row.Date], row.UserID)
}
dates := maps.Keys(respMap)
slices.SortFunc(dates, func(a, b time.Time) bool {
return a.Before(b)
})
var resp codersdk.DeploymentDAUsResponse
for _, date := range fillEmptyDays(dates) {
resp.Entries = append(resp.Entries, codersdk.DAUEntry{
Date: date,
Amount: len(respMap[date]),
})
}
return resp
}
func countUniqueUsers(rows []database.GetTemplateDAUsRow) int {
seen := make(map[uuid.UUID]struct{}, len(rows))
for _, row := range rows {
@ -130,10 +153,19 @@ func (c *Cache) refresh(ctx context.Context) error {
}
var (
deploymentDAUs = codersdk.DeploymentDAUsResponse{}
templateDAUs = make(map[uuid.UUID]codersdk.TemplateDAUsResponse, len(templates))
templateUniqueUsers = make(map[uuid.UUID]int)
templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow)
)
rows, err := c.database.GetDeploymentDAUs(ctx)
if err != nil {
return err
}
deploymentDAUs = convertDeploymentDAUResponse(rows)
c.deploymentDAUResponses.Store(&deploymentDAUs)
for _, template := range templates {
rows, err := c.database.GetTemplateDAUs(ctx, template.ID)
if err != nil {
@ -207,6 +239,11 @@ func (c *Cache) Close() error {
return nil
}
func (c *Cache) DeploymentDAUs() (*codersdk.DeploymentDAUsResponse, bool) {
m := c.deploymentDAUResponses.Load()
return m, m != nil
}
// TemplateDAUs returns an empty response if the template doesn't have users
// or is loading for the first time.
func (c *Cache) TemplateDAUs(id uuid.UUID) (*codersdk.TemplateDAUsResponse, bool) {

28
codersdk/insights.go Normal file
View File

@ -0,0 +1,28 @@
package codersdk
import (
"context"
"encoding/json"
"net/http"
"golang.org/x/xerrors"
)
type DeploymentDAUsResponse struct {
Entries []DAUEntry `json:"entries"`
}
func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil)
if err != nil {
return nil, xerrors.Errorf("execute request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var resp DeploymentDAUsResponse
return &resp, json.NewDecoder(res.Body).Decode(&resp)
}

37
docs/api/insights.md Normal file
View File

@ -0,0 +1,37 @@
# Insights
## Get deployment DAUs
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/insights/daus \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /insights/daus`
### Example responses
> 200 Response
```json
{
"entries": [
{
"amount": 0,
"date": "2019-08-24T14:15:22Z"
}
]
}
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.DeploymentDAUsResponse](schemas.md#codersdkdeploymentdausresponse) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).

View File

@ -2385,6 +2385,25 @@ CreateParameterRequest is a structure used to create a new parameter value for a
| `usage` | string | false | | |
| `value` | integer | false | | |
## codersdk.DeploymentDAUsResponse
```json
{
"entries": [
{
"amount": 0,
"date": "2019-08-24T14:15:22Z"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| --------- | ----------------------------------------------- | -------- | ------------ | ----------- |
| `entries` | array of [codersdk.DAUEntry](#codersdkdauentry) | false | | |
## codersdk.Entitlement
```json

View File

@ -376,6 +376,10 @@
"title": "Files",
"path": "./api/files.md"
},
{
"title": "Insights",
"path": "./api/insights.md"
},
{
"title": "Members",
"path": "./api/members.md"

View File

@ -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> => {

View File

@ -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>

View File

@ -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 }],
}}
/>,

View File

@ -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
})

View File

@ -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 />

View File

@ -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}
/>
</>
)
}

View File

@ -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." }),
}

View File

@ -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>
</>
)
}

View File

@ -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)}
/>

View File

@ -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",
}

View File

@ -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))

View File

@ -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,
}),
},
},
)