mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat!: add sections parameter to template insights (#10010)
This commit is contained in:
+55
-4
@@ -257,6 +257,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
endTimeString = p.String(vals, "", "end_time")
|
||||
intervalString = p.String(vals, "", "interval")
|
||||
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
|
||||
sectionStrings = p.Strings(vals, templateInsightsSectionAsStrings(codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport), "sections")
|
||||
)
|
||||
p.ErrorExcessParams(vals)
|
||||
if len(p.Errors) > 0 {
|
||||
@@ -275,6 +276,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
sections, ok := parseTemplateInsightsSections(ctx, rw, sectionStrings)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var usage database.GetTemplateInsightsRow
|
||||
var appUsage []database.GetTemplateAppInsightsRow
|
||||
@@ -289,7 +294,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
// overhead from a transaction is not worth it.
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
if interval != "" {
|
||||
if interval != "" && slices.Contains(sections, codersdk.TemplateInsightsSectionIntervalReports) {
|
||||
dailyUsage, err = api.Database.GetTemplateInsightsByInterval(egCtx, database.GetTemplateInsightsByIntervalParams{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
@@ -303,6 +308,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
usage, err = api.Database.GetTemplateInsights(egCtx, database.GetTemplateInsightsParams{
|
||||
StartTime: startTime,
|
||||
@@ -315,6 +324,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
appUsage, err = api.Database.GetTemplateAppInsights(egCtx, database.GetTemplateAppInsightsParams{
|
||||
StartTime: startTime,
|
||||
@@ -330,6 +343,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
// Template parameter insights have no risk of inconsistency with the other
|
||||
// insights.
|
||||
eg.Go(func() error {
|
||||
if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
parameterRows, err = api.Database.GetTemplateParameterInsights(ctx, database.GetTemplateParameterInsightsParams{
|
||||
StartTime: startTime,
|
||||
@@ -365,16 +382,20 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
resp := codersdk.TemplateInsightsResponse{
|
||||
Report: codersdk.TemplateInsightsReport{
|
||||
IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
|
||||
}
|
||||
|
||||
if slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
|
||||
resp.Report = &codersdk.TemplateInsightsReport{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TemplateIDs: convertTemplateInsightsTemplateIDs(usage, appUsage),
|
||||
ActiveUsers: convertTemplateInsightsActiveUsers(usage, appUsage),
|
||||
AppsUsage: convertTemplateInsightsApps(usage, appUsage),
|
||||
ParametersUsage: parametersUsage,
|
||||
},
|
||||
IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range dailyUsage {
|
||||
resp.IntervalReports = append(resp.IntervalReports, codersdk.TemplateInsightsIntervalReport{
|
||||
// NOTE(mafredri): This might not be accurate over DST since the
|
||||
@@ -654,3 +675,33 @@ func lastReportIntervalHasAtLeastSixDays(startTime, endTime time.Time) bool {
|
||||
// when the duration can be shorter than 6 days: 5 days 23 hours.
|
||||
return lastReportIntervalDays >= 6*24*time.Hour || startTime.AddDate(0, 0, 6).Equal(endTime)
|
||||
}
|
||||
|
||||
func templateInsightsSectionAsStrings(sections ...codersdk.TemplateInsightsSection) []string {
|
||||
t := make([]string, len(sections))
|
||||
for i, s := range sections {
|
||||
t[i] = string(s)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func parseTemplateInsightsSections(ctx context.Context, rw http.ResponseWriter, sections []string) ([]codersdk.TemplateInsightsSection, bool) {
|
||||
t := make([]codersdk.TemplateInsightsSection, len(sections))
|
||||
for i, s := range sections {
|
||||
switch v := codersdk.TemplateInsightsSection(s); v {
|
||||
case codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport:
|
||||
t[i] = v
|
||||
default:
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Query parameter has invalid value.",
|
||||
Validations: []codersdk.ValidationError{
|
||||
{
|
||||
Field: "sections",
|
||||
Detail: fmt.Sprintf("must be one of %v", []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport}),
|
||||
},
|
||||
},
|
||||
})
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return t, true
|
||||
}
|
||||
|
||||
@@ -1135,6 +1135,30 @@ func TestTemplateInsights_Golden(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three weeks second template only report",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
return codersdk.TemplateInsightsRequest{
|
||||
TemplateIDs: []uuid.UUID{templates[1].id},
|
||||
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
|
||||
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionReport},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three weeks second template only interval reports",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
return codersdk.TemplateInsightsRequest{
|
||||
TemplateIDs: []uuid.UUID{templates[1].id},
|
||||
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
|
||||
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionIntervalReports},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2049,6 +2073,14 @@ func TestTemplateInsights_BadRequest(t *testing.T) {
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
})
|
||||
assert.Error(t, err, "last report interval must have at least 6 days")
|
||||
|
||||
_, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
|
||||
StartTime: today.AddDate(0, 0, -1),
|
||||
EndTime: today,
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
Sections: []codersdk.TemplateInsightsSection{"invalid"},
|
||||
})
|
||||
assert.Error(t, err, "want error for bad section")
|
||||
}
|
||||
|
||||
func TestTemplateInsights_RBAC(t *testing.T) {
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"interval_reports": [
|
||||
{
|
||||
"start_time": "2023-08-01T00:00:00Z",
|
||||
"end_time": "2023-08-08T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"interval": "week",
|
||||
"active_users": 1
|
||||
},
|
||||
{
|
||||
"start_time": "2023-08-08T00:00:00Z",
|
||||
"end_time": "2023-08-15T00:00:00Z",
|
||||
"template_ids": [],
|
||||
"interval": "week",
|
||||
"active_users": 0
|
||||
},
|
||||
{
|
||||
"start_time": "2023-08-15T00:00:00Z",
|
||||
"end_time": "2023-08-22T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"interval": "week",
|
||||
"active_users": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"report": {
|
||||
"start_time": "2023-08-01T00:00:00Z",
|
||||
"end_time": "2023-08-22T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"active_users": 1,
|
||||
"apps_usage": [
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Visual Studio Code",
|
||||
"slug": "vscode",
|
||||
"icon": "/icon/code.svg",
|
||||
"seconds": 3600
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "JetBrains",
|
||||
"slug": "jetbrains",
|
||||
"icon": "/icon/intellij.svg",
|
||||
"seconds": 0
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Web Terminal",
|
||||
"slug": "reconnecting-pty",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 7200
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "SSH",
|
||||
"slug": "ssh",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 10800
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app1",
|
||||
"slug": "app1",
|
||||
"icon": "/icon1.png",
|
||||
"seconds": 21600
|
||||
}
|
||||
],
|
||||
"parameters_usage": []
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -39,6 +39,5 @@
|
||||
}
|
||||
],
|
||||
"parameters_usage": []
|
||||
},
|
||||
"interval_reports": []
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -160,6 +160,5 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"interval_reports": []
|
||||
}
|
||||
}
|
||||
|
||||
+23
-6
@@ -38,6 +38,15 @@ const (
|
||||
InsightsReportIntervalWeek InsightsReportInterval = "week"
|
||||
)
|
||||
|
||||
// TemplateInsightsSection defines the section to be included in the template insights response.
|
||||
type TemplateInsightsSection string
|
||||
|
||||
// TemplateInsightsSection enums.
|
||||
const (
|
||||
TemplateInsightsSectionIntervalReports TemplateInsightsSection = "interval_reports"
|
||||
TemplateInsightsSectionReport TemplateInsightsSection = "report"
|
||||
)
|
||||
|
||||
// UserLatencyInsightsResponse is the response from the user latency insights
|
||||
// endpoint.
|
||||
type UserLatencyInsightsResponse struct {
|
||||
@@ -158,8 +167,8 @@ func (c *Client) UserActivityInsights(ctx context.Context, req UserActivityInsig
|
||||
|
||||
// TemplateInsightsResponse is the response from the template insights endpoint.
|
||||
type TemplateInsightsResponse struct {
|
||||
Report TemplateInsightsReport `json:"report"`
|
||||
IntervalReports []TemplateInsightsIntervalReport `json:"interval_reports"`
|
||||
Report *TemplateInsightsReport `json:"report,omitempty"`
|
||||
IntervalReports []TemplateInsightsIntervalReport `json:"interval_reports,omitempty"`
|
||||
}
|
||||
|
||||
// TemplateInsightsReport is the report from the template insights endpoint.
|
||||
@@ -221,10 +230,11 @@ type TemplateParameterValue struct {
|
||||
}
|
||||
|
||||
type TemplateInsightsRequest struct {
|
||||
StartTime time.Time `json:"start_time" format:"date-time"`
|
||||
EndTime time.Time `json:"end_time" format:"date-time"`
|
||||
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
|
||||
Interval InsightsReportInterval `json:"interval" example:"day"`
|
||||
StartTime time.Time `json:"start_time" format:"date-time"`
|
||||
EndTime time.Time `json:"end_time" format:"date-time"`
|
||||
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
|
||||
Interval InsightsReportInterval `json:"interval" example:"day"`
|
||||
Sections []TemplateInsightsSection `json:"sections" example:"report"`
|
||||
}
|
||||
|
||||
func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsRequest) (TemplateInsightsResponse, error) {
|
||||
@@ -241,6 +251,13 @@ func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsReque
|
||||
if req.Interval != "" {
|
||||
qp.Add("interval", string(req.Interval))
|
||||
}
|
||||
if len(req.Sections) > 0 {
|
||||
var sections []string
|
||||
for _, sec := range req.Sections {
|
||||
sections = append(sections, string(sec))
|
||||
}
|
||||
qp.Add("sections", strings.Join(sections, ","))
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("/api/v2/insights/templates?%s", qp.Encode())
|
||||
resp, err := c.Request(ctx, http.MethodGet, reqURL, nil)
|
||||
|
||||
Generated
+10
-2
@@ -964,12 +964,13 @@ export interface TemplateInsightsRequest {
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly interval: InsightsReportInterval;
|
||||
readonly sections: TemplateInsightsSection[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface TemplateInsightsResponse {
|
||||
readonly report: TemplateInsightsReport;
|
||||
readonly interval_reports: TemplateInsightsIntervalReport[];
|
||||
readonly report?: TemplateInsightsReport;
|
||||
readonly interval_reports?: TemplateInsightsIntervalReport[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
@@ -1877,6 +1878,13 @@ export const ServerSentEventTypes: ServerSentEventType[] = [
|
||||
export type TemplateAppsType = "app" | "builtin";
|
||||
export const TemplateAppsTypes: TemplateAppsType[] = ["app", "builtin"];
|
||||
|
||||
// From codersdk/insights.go
|
||||
export type TemplateInsightsSection = "interval_reports" | "report";
|
||||
export const TemplateInsightsSections: TemplateInsightsSection[] = [
|
||||
"interval_reports",
|
||||
"report",
|
||||
];
|
||||
|
||||
// From codersdk/templates.go
|
||||
export type TemplateRole = "" | "admin" | "use";
|
||||
export const TemplateRoles: TemplateRole[] = ["", "admin", "use"];
|
||||
|
||||
@@ -20,6 +20,7 @@ import { getTemplatePageTitle } from "../utils";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
DAUsResponse,
|
||||
TemplateAppUsage,
|
||||
TemplateInsightsResponse,
|
||||
TemplateParameterUsage,
|
||||
TemplateParameterValue,
|
||||
@@ -105,11 +106,11 @@ export const TemplateInsightsPageView = ({
|
||||
<UserLatencyPanel data={userLatency} />
|
||||
<TemplateUsagePanel
|
||||
sx={{ gridColumn: "span 3" }}
|
||||
data={templateInsights?.report.apps_usage}
|
||||
data={templateInsights?.report?.apps_usage}
|
||||
/>
|
||||
<TemplateParametersUsagePanel
|
||||
sx={{ gridColumn: "span 3" }}
|
||||
data={templateInsights?.report.parameters_usage}
|
||||
data={templateInsights?.report?.parameters_usage}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
@@ -202,7 +203,7 @@ const TemplateUsagePanel = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateInsightsResponse["report"]["apps_usage"] | undefined;
|
||||
data: TemplateAppUsage[] | undefined;
|
||||
}) => {
|
||||
const validUsage = data?.filter((u) => u.seconds > 0);
|
||||
const totalInSeconds =
|
||||
@@ -293,7 +294,7 @@ const TemplateParametersUsagePanel = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateInsightsResponse["report"]["parameters_usage"] | undefined;
|
||||
data: TemplateParameterUsage[] | undefined;
|
||||
}) => {
|
||||
return (
|
||||
<Panel {...panelProps}>
|
||||
@@ -588,12 +589,14 @@ function mapToDAUsResponse(
|
||||
): DAUsResponse {
|
||||
return {
|
||||
tz_hour_offset: 0,
|
||||
entries: data.map((d) => {
|
||||
return {
|
||||
amount: d.active_users,
|
||||
date: d.start_time,
|
||||
};
|
||||
}),
|
||||
entries: data
|
||||
? data.map((d) => {
|
||||
return {
|
||||
amount: d.active_users,
|
||||
date: d.start_time,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user