Skip to content

feat: use active users instead of total users in Template views #3900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Complete frontend
  • Loading branch information
ammario committed Sep 6, 2022
commit c5787bed1ccc0db0970c0609d71849f869a088e0
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ site/out/
*.lock.hcl
.terraform/

.vscode/*
.vscode/*.log
.vscode/launch.json
**/*.swp
.coderv2/*
**/__debug_bin
8 changes: 4 additions & 4 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func create() *cobra.Command {
}

slices.SortFunc(templates, func(a, b codersdk.Template) bool {
return a.WorkspaceOwnerCount > b.WorkspaceOwnerCount
return a.ActiveUserCount > b.ActiveUserCount
})

templateNames := make([]string, 0, len(templates))
Expand All @@ -81,13 +81,13 @@ func create() *cobra.Command {
for _, template := range templates {
templateName := template.Name

if template.WorkspaceOwnerCount > 0 {
if template.ActiveUserCount > 0 {
developerText := "developer"
if template.WorkspaceOwnerCount != 1 {
if template.ActiveUserCount != 1 {
developerText = "developers"
}

templateName += cliui.Styles.Placeholder.Render(fmt.Sprintf(" (used by %d %s)", template.WorkspaceOwnerCount, developerText))
templateName += cliui.Styles.Placeholder.Render(fmt.Sprintf(" (used by %d %s)", template.ActiveUserCount, developerText))
}

templateNames = append(templateNames, templateName)
Expand Down
3 changes: 2 additions & 1 deletion cli/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import (
"sync"
"syscall"

"cdr.dev/slog"
"github.com/pion/udp"
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"cdr.dev/slog"

"github.com/coder/coder/agent"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
Expand Down
4 changes: 2 additions & 2 deletions cli/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func displayTemplates(filterColumns []string, templates ...codersdk.Template) (s
rows := make([]templateTableRow, len(templates))
for i, template := range templates {
suffix := ""
if template.WorkspaceOwnerCount != 1 {
if template.ActiveUserCount != 1 {
suffix = "s"
}

Expand All @@ -76,7 +76,7 @@ func displayTemplates(filterColumns []string, templates ...codersdk.Template) (s
OrganizationID: template.OrganizationID,
Provisioner: template.Provisioner,
ActiveVersionID: template.ActiveVersionID,
UsedBy: cliui.Styles.Fuchsia.Render(fmt.Sprintf("%d developer%s", template.WorkspaceOwnerCount, suffix)),
UsedBy: cliui.Styles.Fuchsia.Render(fmt.Sprintf("%d developer%s", template.ActiveUserCount, suffix)),
MaxTTL: (time.Duration(template.MaxTTLMillis) * time.Millisecond),
MinAutostartInterval: (time.Duration(template.MinAutostartIntervalMillis) * time.Millisecond),
}
Expand Down
22 changes: 11 additions & 11 deletions coderd/metricscache/metricscache.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Cache struct {
templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse]
templateUniqueUsers atomic.Pointer[map[uuid.UUID]int]

doneCh chan struct{}
done chan struct{}
cancel func()

interval time.Duration
Expand All @@ -44,7 +44,7 @@ func New(db database.Store, log slog.Logger, interval time.Duration) *Cache {
c := &Cache{
database: db,
log: log,
doneCh: make(chan struct{}),
done: make(chan struct{}),
cancel: cancel,
interval: interval,
}
Expand Down Expand Up @@ -146,7 +146,7 @@ func (c *Cache) refresh(ctx context.Context) error {
}

func (c *Cache) run(ctx context.Context) {
defer close(c.doneCh)
defer close(c.done)

ticker := time.NewTicker(c.interval)
defer ticker.Stop()
Expand All @@ -173,7 +173,7 @@ func (c *Cache) run(ctx context.Context) {

select {
case <-ticker.C:
case <-c.doneCh:
case <-c.done:
return
case <-ctx.Done():
return
Expand All @@ -183,29 +183,29 @@ func (c *Cache) run(ctx context.Context) {

func (c *Cache) Close() error {
c.cancel()
<-c.doneCh
<-c.done
return 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 {
func (c *Cache) TemplateDAUs(id uuid.UUID) (*codersdk.TemplateDAUsResponse, bool) {
m := c.templateDAUResponses.Load()
if m == nil {
// Data loading.
return codersdk.TemplateDAUsResponse{}
return nil, false
}

resp, ok := (*m)[id]
if !ok {
// Probably no data.
return codersdk.TemplateDAUsResponse{}
return nil, false
}
return resp
return &resp, true
}

// TemplateUniqueUsers returns the total number of unique users for the template,
// from all the Cache data.
// TemplateUniqueUsers returns the number of unique Template users
// from all Cache data.
func (c *Cache) TemplateUniqueUsers(id uuid.UUID) (int, bool) {
m := c.templateUniqueUsers.Load()
if m == nil {
Expand Down
7 changes: 5 additions & 2 deletions coderd/metricscache/metricscache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,22 +173,25 @@ func TestCache(t *testing.T) {

gotUniqueUsers, ok := cache.TemplateUniqueUsers(templateID)
require.False(t, ok, "template shouldn't have loaded yet")
require.EqualValues(t, -1, gotUniqueUsers)

for _, row := range tt.args.rows {
row.TemplateID = templateID
db.InsertAgentStat(context.Background(), row)
}

require.Eventuallyf(t, func() bool {
return len(cache.TemplateDAUs(templateID).Entries) > 0
_, ok := cache.TemplateDAUs(templateID)
return ok
}, testutil.WaitShort, testutil.IntervalMedium,
"TemplateDAUs never populated",
)

gotUniqueUsers, ok = cache.TemplateUniqueUsers(templateID)
require.True(t, ok)

gotEntries := cache.TemplateDAUs(templateID)
gotEntries, ok := cache.TemplateDAUs(templateID)
require.True(t, ok)
require.Equal(t, tt.want.entries, gotEntries.Entries)
require.Equal(t, tt.want.uniqueUsers, gotUniqueUsers)
})
Expand Down
28 changes: 16 additions & 12 deletions coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (api *API) template(rw http.ResponseWriter, r *http.Request) {
return
}

httpapi.Write(rw, http.StatusOK, convertTemplate(template, count, createdByNameMap[template.ID.String()]))
httpapi.Write(rw, http.StatusOK, api.convertTemplate(template, count, createdByNameMap[template.ID.String()]))
}

func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -268,7 +268,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return xerrors.Errorf("get creator name: %w", err)
}

template = convertTemplate(dbTemplate, 0, createdByNameMap[dbTemplate.ID.String()])
template = api.convertTemplate(dbTemplate, 0, createdByNameMap[dbTemplate.ID.String()])
return nil
})
if err != nil {
Expand Down Expand Up @@ -339,7 +339,7 @@ func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request)
return
}

httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts, createdByNameMap))
httpapi.Write(rw, http.StatusOK, api.convertTemplates(templates, workspaceCounts, createdByNameMap))
}

func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -393,7 +393,7 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
return
}

httpapi.Write(rw, http.StatusOK, convertTemplate(template, count, createdByNameMap[template.ID.String()]))
httpapi.Write(rw, http.StatusOK, api.convertTemplate(template, count, createdByNameMap[template.ID.String()]))
}

func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -514,7 +514,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return
}

httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count, createdByNameMap[updated.ID.String()]))
httpapi.Write(rw, http.StatusOK, api.convertTemplate(updated, count, createdByNameMap[updated.ID.String()]))
}

func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) {
Expand All @@ -524,7 +524,7 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) {
return
}

resp := api.metricsCache.TemplateDAUs(template.ID)
resp, _ := api.metricsCache.TemplateDAUs(template.ID)
if resp.Entries == nil {
resp.Entries = []codersdk.DAUEntry{}
}
Expand Down Expand Up @@ -683,7 +683,7 @@ func getCreatedByNamesByTemplateIDs(ctx context.Context, db database.Store, temp
return creators, nil
}

func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow, createdByNameMap map[string]string) []codersdk.Template {
func (api *API) convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow, createdByNameMap map[string]string) []codersdk.Template {
apiTemplates := make([]codersdk.Template, 0, len(templates))

for _, template := range templates {
Expand All @@ -692,24 +692,27 @@ func convertTemplates(templates []database.Template, workspaceCounts []database.
if workspaceCount.TemplateID.String() != template.ID.String() {
continue
}
apiTemplates = append(apiTemplates, convertTemplate(template, uint32(workspaceCount.Count), createdByNameMap[template.ID.String()]))
apiTemplates = append(apiTemplates, api.convertTemplate(template, uint32(workspaceCount.Count), createdByNameMap[template.ID.String()]))
found = true
break
}
if !found {
apiTemplates = append(apiTemplates, convertTemplate(template, uint32(0), createdByNameMap[template.ID.String()]))
apiTemplates = append(apiTemplates, api.convertTemplate(template, uint32(0), createdByNameMap[template.ID.String()]))
}
}

// Sort templates by WorkspaceOwnerCount DESC
// Sort templates by ActiveUserCount DESC
sort.SliceStable(apiTemplates, func(i, j int) bool {
return apiTemplates[i].WorkspaceOwnerCount > apiTemplates[j].WorkspaceOwnerCount
return apiTemplates[i].ActiveUserCount > apiTemplates[j].ActiveUserCount
})

return apiTemplates
}

func convertTemplate(template database.Template, workspaceOwnerCount uint32, createdByName string) codersdk.Template {
func (api *API) convertTemplate(
template database.Template, workspaceOwnerCount uint32, createdByName string,
) codersdk.Template {
activeCount, _ := api.metricsCache.TemplateUniqueUsers(template.ID)
return codersdk.Template{
ID: template.ID,
CreatedAt: template.CreatedAt,
Expand All @@ -719,6 +722,7 @@ func convertTemplate(template database.Template, workspaceOwnerCount uint32, cre
Provisioner: codersdk.ProvisionerType(template.Provisioner),
ActiveVersionID: template.ActiveVersionID,
WorkspaceOwnerCount: workspaceOwnerCount,
ActiveUserCount: activeCount,
Description: template.Description,
Icon: template.Icon,
MaxTTLMillis: time.Duration(template.MaxTtl).Milliseconds(),
Expand Down
16 changes: 12 additions & 4 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ func TestTemplateDAUs(t *testing.T) {
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Equal(t, -1, template.ActiveUserCount)

coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
Expand Down Expand Up @@ -617,7 +619,7 @@ func TestTemplateDAUs(t *testing.T) {
require.NoError(t, err)
_ = sshConn.Close()

want := &codersdk.TemplateDAUsResponse{
wantDAUs := &codersdk.TemplateDAUsResponse{
Entries: []codersdk.DAUEntry{
{

Expand All @@ -629,12 +631,18 @@ func TestTemplateDAUs(t *testing.T) {
require.Eventuallyf(t, func() bool {
daus, err = client.TemplateDAUs(ctx, template.ID)
require.NoError(t, err)

return assert.ObjectsAreEqual(want, daus)
return len(daus.Entries) > 0
},
testutil.WaitShort, testutil.IntervalFast,
"got %+v != %+v", daus, want,
"template daus never loaded",
)
gotDAUs, err := client.TemplateDAUs(ctx, template.ID)
require.NoError(t, err)
require.Equal(t, gotDAUs, wantDAUs)

template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
require.Equal(t, 1, template.ActiveUserCount)

workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
Expand Down
30 changes: 16 additions & 14 deletions codersdk/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,22 @@ import (
// Template is the JSON representation of a Coder template. This type matches the
// database object for now, but is abstracted for ease of change later on.
type Template struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Provisioner ProvisionerType `json:"provisioner"`
ActiveVersionID uuid.UUID `json:"active_version_id"`
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
Description string `json:"description"`
Icon string `json:"icon"`
MaxTTLMillis int64 `json:"max_ttl_ms"`
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"`
CreatedByID uuid.UUID `json:"created_by_id"`
CreatedByName string `json:"created_by_name"`
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Provisioner ProvisionerType `json:"provisioner"`
ActiveVersionID uuid.UUID `json:"active_version_id"`
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
// ActiveUserCount is set to -1 when loading.
ActiveUserCount int `json:"active_user_count"`
Description string `json:"description"`
Icon string `json:"icon"`
MaxTTLMillis int64 `json:"max_ttl_ms"`
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"`
CreatedByID uuid.UUID `json:"created_by_id"`
CreatedByName string `json:"created_by_name"`
}

type UpdateActiveTemplateVersion struct {
Expand Down
1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export interface Template {
readonly provisioner: ProvisionerType
readonly active_version_id: string
readonly workspace_owner_count: number
readonly active_user_count: number
readonly description: string
readonly icon: string
readonly max_ttl_ms: number
Expand Down
11 changes: 10 additions & 1 deletion site/src/components/TemplateStats/TemplateStats.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ export const UsedByMany = Template.bind({})
UsedByMany.args = {
template: {
...Mocks.MockTemplate,
workspace_owner_count: 15,
active_user_count: 15,
},
activeVersion: Mocks.MockTemplateVersion,
}

export const ActiveUsersNotLoaded = Template.bind({})
ActiveUsersNotLoaded.args = {
template: {
...Mocks.MockTemplate,
active_user_count: -1,
},
activeVersion: Mocks.MockTemplateVersion,
}
Expand Down
7 changes: 3 additions & 4 deletions site/src/components/TemplateStats/TemplateStats.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { makeStyles } from "@material-ui/core/styles"
import { FC } from "react"
import { createDayString } from "util/createDayString"
import { formatTemplateActiveDevelopers } from "util/templates"
import { Template, TemplateVersion } from "../../api/typesGenerated"
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"

Expand All @@ -27,10 +28,8 @@ export const TemplateStats: FC<TemplateStatsProps> = ({ template, activeVersion
<span className={styles.statsLabel}>{Language.usedByLabel}</span>

<span className={styles.statsValue}>
{template.workspace_owner_count}{" "}
{template.workspace_owner_count === 1
? Language.developerSingular
: Language.developerPlural}
{formatTemplateActiveDevelopers(template.active_user_count)}{" "}
{template.active_user_count === 1 ? Language.developerSingular : Language.developerPlural}
</span>
</div>
<div className={styles.statsDivider} />
Expand Down
Loading