diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 6107396eaa..9d46759ecf 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -814,11 +814,17 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { return TypescriptType{}, xerrors.Errorf("array: %w", err) } genValue := "" + + // Always wrap in parentheses for proper scoped types. + // Running prettier on this output will remove redundant parenthesis, + // so this makes our decision-making easier. + // The example that breaks without this is: + // readonly readonly string[][] if underlying.GenericValue != "" { - genValue = underlying.GenericValue + "[]" + genValue = "(readonly " + underlying.GenericValue + "[])" } return TypescriptType{ - ValueType: underlying.ValueType + "[]", + ValueType: "(readonly " + underlying.ValueType + "[])", GenericValue: genValue, AboveTypeLine: underlying.AboveTypeLine, GenericTypes: underlying.GenericTypes, diff --git a/scripts/apitypings/main_test.go b/scripts/apitypings/main_test.go index d777f18950..89f75f1148 100644 --- a/scripts/apitypings/main_test.go +++ b/scripts/apitypings/main_test.go @@ -7,6 +7,7 @@ package main import ( + "flag" "os" "path/filepath" "strings" @@ -15,6 +16,9 @@ import ( "github.com/stretchr/testify/require" ) +// updateGoldenFiles is a flag that can be set to update golden files. +var updateGoldenFiles = flag.Bool("update", false, "Update golden files") + func TestGeneration(t *testing.T) { t.Parallel() files, err := os.ReadDir("testdata") @@ -37,7 +41,13 @@ func TestGeneration(t *testing.T) { require.NoErrorf(t, err, "read file %s", golden) expectedString := strings.TrimSpace(string(expected)) output = strings.TrimSpace(output) - require.Equal(t, expectedString, output, "matched output") + if *updateGoldenFiles { + // nolint:gosec + err := os.WriteFile(golden, []byte(output), 0o644) + require.NoError(t, err, "write golden file") + } else { + require.Equal(t, expectedString, output, "matched output") + } }) } } diff --git a/scripts/apitypings/testdata/enums/enums.go b/scripts/apitypings/testdata/enums/enums.go index 777a91441a..832bd8ad3e 100644 --- a/scripts/apitypings/testdata/enums/enums.go +++ b/scripts/apitypings/testdata/enums/enums.go @@ -1,8 +1,8 @@ package codersdk type ( - Enum string - Enums []Enum + Enum string + EnumSliceType []Enum ) const ( diff --git a/scripts/apitypings/testdata/enums/enums.ts b/scripts/apitypings/testdata/enums/enums.ts index 2fc20f0e33..09a6c43ce4 100644 --- a/scripts/apitypings/testdata/enums/enums.ts +++ b/scripts/apitypings/testdata/enums/enums.ts @@ -1,5 +1,5 @@ // From codersdk/enums.go -export type Enums = Enum[] +export type EnumSliceType = (readonly Enum[]) // From codersdk/enums.go export type Enum = "bar" | "baz" | "foo" | "qux" diff --git a/scripts/apitypings/testdata/genericmap/genericmap.go b/scripts/apitypings/testdata/genericmap/genericmap.go index 721ba95313..dc80d1f8a4 100644 --- a/scripts/apitypings/testdata/genericmap/genericmap.go +++ b/scripts/apitypings/testdata/genericmap/genericmap.go @@ -1,22 +1,22 @@ package codersdk -type Foo struct { - Bar string `json:"bar"` -} - type Buzz struct { Foo `json:"foo"` Bazz string `json:"bazz"` } -type Custom interface { - Foo | Buzz +type Foo struct { + Bar string `json:"bar"` } type FooBuzz[R Custom] struct { Something []R `json:"something"` } +type Custom interface { + Foo | Buzz +} + // Not yet supported //type FooBuzzMap[R Custom] struct { // Something map[string]R `json:"something"` diff --git a/scripts/apitypings/testdata/genericmap/genericmap.ts b/scripts/apitypings/testdata/genericmap/genericmap.ts index 9ceca8b44d..417dd7e2e8 100644 --- a/scripts/apitypings/testdata/genericmap/genericmap.ts +++ b/scripts/apitypings/testdata/genericmap/genericmap.ts @@ -11,8 +11,8 @@ export interface Foo { // From codersdk/genericmap.go export interface FooBuzz { - readonly something: R[] + readonly something: (readonly R[]) } // From codersdk/genericmap.go -export type Custom = Foo | Buzz +export type Custom = Foo | Buzz \ No newline at end of file diff --git a/scripts/apitypings/testdata/generics/generics.ts b/scripts/apitypings/testdata/generics/generics.ts index 57cfdb7bc5..2a11bb5cd3 100644 --- a/scripts/apitypings/testdata/generics/generics.ts +++ b/scripts/apitypings/testdata/generics/generics.ts @@ -33,9 +33,9 @@ export interface Static { } // From codersdk/generics.go -export type Custom = string | boolean | number | string[] | null +export type Custom = string | boolean | number | (readonly string[]) | null // From codersdk/generics.go export type Single = string -export type comparable = boolean | number | string | any +export type comparable = boolean | number | string | any \ No newline at end of file diff --git a/scripts/apitypings/testdata/genericslice/genericslice.go b/scripts/apitypings/testdata/genericslice/genericslice.go new file mode 100644 index 0000000000..ae439026a2 --- /dev/null +++ b/scripts/apitypings/testdata/genericslice/genericslice.go @@ -0,0 +1,10 @@ +package codersdk + +type Bar struct { + Bar string +} + +type Foo[R any] struct { + Slice []R + TwoD [][]R +} diff --git a/scripts/apitypings/testdata/genericslice/genericslice.ts b/scripts/apitypings/testdata/genericslice/genericslice.ts new file mode 100644 index 0000000000..fc88c04a87 --- /dev/null +++ b/scripts/apitypings/testdata/genericslice/genericslice.ts @@ -0,0 +1,10 @@ +// From codersdk/genericslice.go +export interface Bar { + readonly Bar: string +} + +// From codersdk/genericslice.go +export interface Foo { + readonly Slice: (readonly R[]) + readonly TwoD: (readonly (readonly R[])[]) +} \ No newline at end of file diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index be751559f2..54f4e57d77 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -4,8 +4,8 @@ // From codersdk/templates.go export interface ACLAvailable { - readonly users: ReducedUser[]; - readonly groups: Group[]; + readonly users: readonly ReducedUser[]; + readonly groups: readonly Group[]; } // From codersdk/apikey.go @@ -49,7 +49,7 @@ export interface AppearanceConfig { readonly application_name: string; readonly logo_url: string; readonly service_banner: ServiceBannerConfig; - readonly support_links?: LinkConfig[]; + readonly support_links?: readonly LinkConfig[]; } // From codersdk/templates.go @@ -60,7 +60,7 @@ export interface ArchiveTemplateVersionsRequest { // From codersdk/templates.go export interface ArchiveTemplateVersionsResponse { readonly template_id: string; - readonly archived_ids: string[]; + readonly archived_ids: readonly string[]; } // From codersdk/roles.go @@ -108,7 +108,7 @@ export interface AuditLog { // From codersdk/audit.go export interface AuditLogResponse { - readonly audit_logs: AuditLog[]; + readonly audit_logs: readonly AuditLog[]; readonly count: number; } @@ -153,7 +153,7 @@ export type AuthorizationResponse = Record; // From codersdk/deployment.go export interface AvailableExperiments { - readonly safe: Experiment[]; + readonly safe: readonly Experiment[]; } // From codersdk/deployment.go @@ -241,8 +241,8 @@ export interface CreateTemplateRequest { // From codersdk/templateversions.go export interface CreateTemplateVersionDryRunRequest { readonly workspace_name: string; - readonly rich_parameter_values: WorkspaceBuildParameter[]; - readonly user_variable_values?: VariableValue[]; + readonly rich_parameter_values: readonly WorkspaceBuildParameter[]; + readonly user_variable_values?: readonly VariableValue[]; } // From codersdk/organizations.go @@ -255,7 +255,7 @@ export interface CreateTemplateVersionRequest { readonly example_id?: string; readonly provisioner: ProvisionerType; readonly tags: Record; - readonly user_variable_values?: VariableValue[]; + readonly user_variable_values?: readonly VariableValue[]; } // From codersdk/audit.go @@ -292,7 +292,7 @@ export interface CreateWorkspaceBuildRequest { readonly dry_run?: boolean; readonly state?: string; readonly orphan?: boolean; - readonly rich_parameter_values?: WorkspaceBuildParameter[]; + readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly log_level?: ProvisionerLogLevel; } @@ -310,7 +310,7 @@ export interface CreateWorkspaceRequest { readonly name: string; readonly autostart_schedule?: string; readonly ttl_ms?: number; - readonly rich_parameter_values?: WorkspaceBuildParameter[]; + readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly automatic_updates?: AutomaticUpdates; } @@ -327,7 +327,7 @@ export interface DAURequest { // From codersdk/deployment.go export interface DAUsResponse { - readonly entries: DAUEntry[]; + readonly entries: readonly DAUEntry[]; readonly tz_hour_offset: number; } @@ -434,7 +434,7 @@ export interface DeploymentValues { readonly session_lifetime?: SessionLifetime; readonly disable_password_auth?: boolean; readonly support?: SupportConfig; - readonly external_auth?: ExternalAuthConfig[]; + readonly external_auth?: readonly ExternalAuthConfig[]; readonly config_ssh?: SSHConfig; readonly wgtunnel_host?: string; readonly disable_owner_workspace_exec?: boolean; @@ -453,8 +453,8 @@ export interface DeploymentValues { // From codersdk/deployment.go export interface Entitlements { readonly features: Record; - readonly warnings: string[]; - readonly errors: string[]; + readonly warnings: readonly string[]; + readonly errors: readonly string[]; readonly has_license: boolean; readonly trial: boolean; readonly require_telemetry: boolean; @@ -462,7 +462,7 @@ export interface Entitlements { } // From codersdk/deployment.go -export type Experiments = Experiment[]; +export type Experiments = readonly Experiment[]; // From codersdk/externalauth.go export interface ExternalAuth { @@ -471,7 +471,7 @@ export interface ExternalAuth { readonly display_name: string; readonly user?: ExternalAuthUser; readonly app_installable: boolean; - readonly installations: ExternalAuthAppInstallation[]; + readonly installations: readonly ExternalAuthAppInstallation[]; readonly app_install_url: string; } @@ -493,8 +493,8 @@ export interface ExternalAuthConfig { readonly app_install_url: string; readonly app_installations_url: string; readonly no_refresh: boolean; - readonly scopes: string[]; - readonly extra_token_keys: string[]; + readonly scopes: readonly string[]; + readonly extra_token_keys: readonly string[]; readonly device_flow: boolean; readonly device_code_url: string; readonly regex: string; @@ -561,7 +561,7 @@ export interface GenerateAPIKeyResponse { // From codersdk/users.go export interface GetUsersResponse { - readonly users: User[]; + readonly users: readonly User[]; readonly count: number; } @@ -579,7 +579,7 @@ export interface Group { readonly name: string; readonly display_name: string; readonly organization_id: string; - readonly members: ReducedUser[]; + readonly members: readonly ReducedUser[]; readonly avatar_url: string; readonly quota_allowance: number; readonly source: GroupSource; @@ -638,8 +638,8 @@ export interface LinkConfig { // From codersdk/externalauth.go export interface ListUserExternalAuthResponse { - readonly providers: ExternalAuthLinkProvider[]; - readonly links: ExternalAuthLink[]; + readonly providers: readonly ExternalAuthLinkProvider[]; + readonly links: readonly ExternalAuthLink[]; } // From codersdk/deployment.go @@ -753,7 +753,7 @@ export interface OIDCConfig { readonly groups_field: string; readonly group_mapping: Record; readonly user_role_field: string; - readonly user_role_mapping: Record; + readonly user_role_mapping: Record; readonly user_roles_default: string[]; readonly sign_in_text: string; readonly icon_url: string; @@ -775,7 +775,7 @@ export interface OrganizationMember { readonly organization_id: string; readonly created_at: string; readonly updated_at: string; - readonly roles: Role[]; + readonly roles: readonly Role[]; } // From codersdk/pagination.go @@ -787,8 +787,8 @@ export interface Pagination { // From codersdk/groups.go export interface PatchGroupRequest { - readonly add_users: string[]; - readonly remove_users: string[]; + readonly add_users: readonly string[]; + readonly remove_users: readonly string[]; readonly name: string; readonly display_name?: string; readonly avatar_url?: string; @@ -850,7 +850,7 @@ export interface ProvisionerDaemon { readonly name: string; readonly version: string; readonly api_version: string; - readonly provisioners: ProvisionerType[]; + readonly provisioners: readonly ProvisionerType[]; readonly tags: Record; } @@ -883,8 +883,8 @@ export interface ProvisionerJobLog { // From codersdk/workspaceproxy.go export interface ProxyHealthReport { - readonly errors: string[]; - readonly warnings: string[]; + readonly errors: readonly string[]; + readonly warnings: readonly string[]; } // From codersdk/workspaces.go @@ -929,7 +929,7 @@ export interface Region { // From codersdk/workspaceproxy.go export interface RegionsResponse { - readonly regions: R[]; + readonly regions: readonly R[]; } // From codersdk/replicas.go @@ -952,7 +952,7 @@ export interface ResolveAutostartResponse { export interface Response { readonly message: string; readonly detail?: string; - readonly validations?: ValidationError[]; + readonly validations?: readonly ValidationError[]; } // From codersdk/roles.go @@ -1005,7 +1005,7 @@ export interface SessionLifetime { // From codersdk/deployment.go export interface SupportConfig { - readonly links: LinkConfig[]; + readonly links: readonly LinkConfig[]; } // From codersdk/deployment.go @@ -1070,13 +1070,13 @@ export interface Template { // From codersdk/templates.go export interface TemplateACL { - readonly users: TemplateUser[]; - readonly group: TemplateGroup[]; + readonly users: readonly TemplateUser[]; + readonly group: readonly TemplateGroup[]; } // From codersdk/insights.go export interface TemplateAppUsage { - readonly template_ids: string[]; + readonly template_ids: readonly string[]; readonly type: TemplateAppsType; readonly display_name: string; readonly slug: string; @@ -1086,12 +1086,12 @@ export interface TemplateAppUsage { // From codersdk/templates.go export interface TemplateAutostartRequirement { - readonly days_of_week: string[]; + readonly days_of_week: readonly string[]; } // From codersdk/templates.go export interface TemplateAutostopRequirement { - readonly days_of_week: string[]; + readonly days_of_week: readonly string[]; readonly weeks: number; } @@ -1108,7 +1108,7 @@ export interface TemplateExample { readonly name: string; readonly description: string; readonly icon: string; - readonly tags: string[]; + readonly tags: readonly string[]; readonly markdown: string; } @@ -1121,7 +1121,7 @@ export interface TemplateGroup extends Group { export interface TemplateInsightsIntervalReport { readonly start_time: string; readonly end_time: string; - readonly template_ids: string[]; + readonly template_ids: readonly string[]; readonly interval: InsightsReportInterval; readonly active_users: number; } @@ -1130,36 +1130,36 @@ export interface TemplateInsightsIntervalReport { export interface TemplateInsightsReport { readonly start_time: string; readonly end_time: string; - readonly template_ids: string[]; + readonly template_ids: readonly string[]; readonly active_users: number; - readonly apps_usage: TemplateAppUsage[]; - readonly parameters_usage: TemplateParameterUsage[]; + readonly apps_usage: readonly TemplateAppUsage[]; + readonly parameters_usage: readonly TemplateParameterUsage[]; } // From codersdk/insights.go export interface TemplateInsightsRequest { readonly start_time: string; readonly end_time: string; - readonly template_ids: string[]; + readonly template_ids: readonly string[]; readonly interval: InsightsReportInterval; - readonly sections: TemplateInsightsSection[]; + readonly sections: readonly TemplateInsightsSection[]; } // From codersdk/insights.go export interface TemplateInsightsResponse { readonly report?: TemplateInsightsReport; - readonly interval_reports?: TemplateInsightsIntervalReport[]; + readonly interval_reports?: readonly TemplateInsightsIntervalReport[]; } // From codersdk/insights.go export interface TemplateParameterUsage { - readonly template_ids: string[]; + readonly template_ids: readonly string[]; readonly display_name: string; readonly name: string; readonly type: string; readonly description: string; - readonly options?: TemplateVersionParameterOption[]; - readonly values: TemplateParameterValue[]; + readonly options?: readonly TemplateVersionParameterOption[]; + readonly values: readonly TemplateParameterValue[]; } // From codersdk/insights.go @@ -1186,7 +1186,7 @@ export interface TemplateVersion { readonly readme: string; readonly created_by: MinimalUser; readonly archived: boolean; - readonly warnings?: TemplateVersionWarning[]; + readonly warnings?: readonly TemplateVersionWarning[]; } // From codersdk/templateversions.go @@ -1210,7 +1210,7 @@ export interface TemplateVersionParameter { readonly mutable: boolean; readonly default_value: string; readonly icon: string; - readonly options: TemplateVersionParameterOption[]; + readonly options: readonly TemplateVersionParameterOption[]; readonly validation_error?: string; readonly validation_regex?: string; readonly validation_min?: number; @@ -1290,7 +1290,7 @@ export interface UpdateCheckResponse { // From codersdk/users.go export interface UpdateRoles { - readonly roles: string[]; + readonly roles: readonly string[]; } // From codersdk/templates.go @@ -1391,13 +1391,13 @@ export interface UpsertWorkspaceAgentPortShareRequest { // From codersdk/users.go export interface User extends ReducedUser { - readonly organization_ids: string[]; - readonly roles: Role[]; + readonly organization_ids: readonly string[]; + readonly roles: readonly Role[]; } // From codersdk/insights.go export interface UserActivity { - readonly template_ids: string[]; + readonly template_ids: readonly string[]; readonly user_id: string; readonly username: string; readonly avatar_url: string; @@ -1408,15 +1408,15 @@ export interface UserActivity { export interface UserActivityInsightsReport { readonly start_time: string; readonly end_time: string; - readonly template_ids: string[]; - readonly users: UserActivity[]; + readonly template_ids: readonly string[]; + readonly users: readonly UserActivity[]; } // From codersdk/insights.go export interface UserActivityInsightsRequest { readonly start_time: string; readonly end_time: string; - readonly template_ids: string[]; + readonly template_ids: readonly string[]; } // From codersdk/insights.go @@ -1426,7 +1426,7 @@ export interface UserActivityInsightsResponse { // From codersdk/insights.go export interface UserLatency { - readonly template_ids: string[]; + readonly template_ids: readonly string[]; readonly user_id: string; readonly username: string; readonly avatar_url: string; @@ -1437,15 +1437,15 @@ export interface UserLatency { export interface UserLatencyInsightsReport { readonly start_time: string; readonly end_time: string; - readonly template_ids: string[]; - readonly users: UserLatency[]; + readonly template_ids: readonly string[]; + readonly users: readonly UserLatency[]; } // From codersdk/insights.go export interface UserLatencyInsightsRequest { readonly start_time: string; readonly end_time: string; - readonly template_ids: string[]; + readonly template_ids: readonly string[]; } // From codersdk/insights.go @@ -1482,8 +1482,8 @@ export interface UserQuietHoursScheduleResponse { // From codersdk/users.go export interface UserRoles { - readonly roles: string[]; - readonly organization_roles: Record; + readonly roles: readonly string[]; + readonly organization_roles: Record; } // From codersdk/users.go @@ -1557,15 +1557,15 @@ export interface WorkspaceAgent { readonly expanded_directory?: string; readonly version: string; readonly api_version: string; - readonly apps: WorkspaceApp[]; + readonly apps: readonly WorkspaceApp[]; readonly latency?: Record; readonly connection_timeout_seconds: number; readonly troubleshooting_url: string; - readonly subsystems: AgentSubsystem[]; + readonly subsystems: readonly AgentSubsystem[]; readonly health: WorkspaceAgentHealth; - readonly display_apps: DisplayApp[]; - readonly log_sources: WorkspaceAgentLogSource[]; - readonly scripts: WorkspaceAgentScript[]; + readonly display_apps: readonly DisplayApp[]; + readonly log_sources: readonly WorkspaceAgentLogSource[]; + readonly scripts: readonly WorkspaceAgentScript[]; readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior; } @@ -1584,7 +1584,7 @@ export interface WorkspaceAgentListeningPort { // From codersdk/workspaceagents.go export interface WorkspaceAgentListeningPortsResponse { - readonly ports: WorkspaceAgentListeningPort[]; + readonly ports: readonly WorkspaceAgentListeningPort[]; } // From codersdk/workspaceagents.go @@ -1639,7 +1639,7 @@ export interface WorkspaceAgentPortShare { // From codersdk/workspaceagentportshare.go export interface WorkspaceAgentPortShares { - readonly shares: WorkspaceAgentPortShare[]; + readonly shares: readonly WorkspaceAgentPortShare[]; } // From codersdk/workspaceagents.go @@ -1688,7 +1688,7 @@ export interface WorkspaceBuild { readonly initiator_name: string; readonly job: ProvisionerJob; readonly reason: BuildReason; - readonly resources: WorkspaceResource[]; + readonly resources: readonly WorkspaceResource[]; readonly deadline?: string; readonly max_deadline?: string; readonly status: WorkspaceStatus; @@ -1732,7 +1732,7 @@ export interface WorkspaceFilter { // From codersdk/workspaces.go export interface WorkspaceHealth { readonly healthy: boolean; - readonly failing_agents: string[]; + readonly failing_agents: readonly string[]; } // From codersdk/workspaces.go @@ -1780,8 +1780,8 @@ export interface WorkspaceResource { readonly name: string; readonly hide: boolean; readonly icon: string; - readonly agents?: WorkspaceAgent[]; - readonly metadata?: WorkspaceResourceMetadata[]; + readonly agents?: readonly WorkspaceAgent[]; + readonly metadata?: readonly WorkspaceResourceMetadata[]; readonly daily_cost: number; } @@ -1799,7 +1799,7 @@ export interface WorkspacesRequest extends Pagination { // From codersdk/workspaces.go export interface WorkspacesResponse { - readonly workspaces: Workspace[]; + readonly workspaces: readonly Workspace[]; readonly count: number; } @@ -2285,7 +2285,7 @@ export type RegionTypes = Region | WorkspaceProxy; export interface AccessURLReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; readonly dismissed: boolean; readonly access_url: string; readonly reachable: boolean; @@ -2298,14 +2298,14 @@ export interface AccessURLReport { export interface DERPHealthReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; readonly dismissed: boolean; readonly regions: Record; // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly netcheck?: any; readonly netcheck_err?: string; - readonly netcheck_logs: string[]; + readonly netcheck_logs: readonly string[]; readonly error?: string; } @@ -2313,7 +2313,7 @@ export interface DERPHealthReport { export interface DERPNodeReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; // Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly node?: any; @@ -2324,8 +2324,8 @@ export interface DERPNodeReport { readonly round_trip_ping: string; readonly round_trip_ping_ms: number; readonly uses_websocket: boolean; - readonly client_logs: string[][]; - readonly client_errs: string[][]; + readonly client_logs: readonly (readonly string[])[]; + readonly client_errs: readonly (readonly string[])[]; readonly error?: string; readonly stun: STUNReport; } @@ -2334,11 +2334,11 @@ export interface DERPNodeReport { export interface DERPRegionReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; // Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly region?: any; - readonly node_reports: DERPNodeReport[]; + readonly node_reports: readonly DERPNodeReport[]; readonly error?: string; } @@ -2346,7 +2346,7 @@ export interface DERPRegionReport { export interface DatabaseReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; readonly dismissed: boolean; readonly reachable: boolean; readonly latency: string; @@ -2357,7 +2357,7 @@ export interface DatabaseReport { // From healthsdk/healthsdk.go export interface HealthSettings { - readonly dismissed_healthchecks: HealthSection[]; + readonly dismissed_healthchecks: readonly HealthSection[]; } // From healthsdk/healthsdk.go @@ -2365,7 +2365,7 @@ export interface HealthcheckReport { readonly time: string; readonly healthy: boolean; readonly severity: HealthSeverity; - readonly failing_sections: HealthSection[]; + readonly failing_sections: readonly HealthSection[]; readonly derp: DERPHealthReport; readonly access_url: AccessURLReport; readonly websocket: WebsocketReport; @@ -2378,16 +2378,16 @@ export interface HealthcheckReport { // From healthsdk/healthsdk.go export interface ProvisionerDaemonsReport { readonly severity: HealthSeverity; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; readonly dismissed: boolean; readonly error?: string; - readonly items: ProvisionerDaemonsReportItem[]; + readonly items: readonly ProvisionerDaemonsReportItem[]; } // From healthsdk/healthsdk.go export interface ProvisionerDaemonsReportItem { readonly provisioner_daemon: ProvisionerDaemon; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; } // From healthsdk/healthsdk.go @@ -2399,14 +2399,14 @@ export interface STUNReport { // From healthsdk/healthsdk.go export interface UpdateHealthSettings { - readonly dismissed_healthchecks: HealthSection[]; + readonly dismissed_healthchecks: readonly HealthSection[]; } // From healthsdk/healthsdk.go export interface WebsocketReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: string[]; + readonly warnings: readonly string[]; readonly dismissed: boolean; readonly body: string; readonly code: number; @@ -2417,7 +2417,7 @@ export interface WebsocketReport { export interface WorkspaceProxyReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: HealthMessage[]; + readonly warnings: readonly HealthMessage[]; readonly dismissed: boolean; readonly error?: string; readonly workspace_proxies: RegionsResponse; @@ -2520,13 +2520,13 @@ export interface SerpentOption { readonly value?: any; readonly annotations?: SerpentAnnotations; readonly group?: SerpentGroup; - readonly use_instead?: SerpentOption[]; + readonly use_instead?: readonly SerpentOption[]; readonly hidden?: boolean; readonly value_source?: SerpentValueSource; } // From serpent/option.go -export type SerpentOptionSet = SerpentOption[]; +export type SerpentOptionSet = readonly SerpentOption[]; // From serpent/option.go export type SerpentValueSource = "" | "default" | "env" | "flag" | "yaml"; diff --git a/site/src/components/ActiveUserChart/ActiveUserChart.tsx b/site/src/components/ActiveUserChart/ActiveUserChart.tsx index c66f882bf5..2377d965eb 100644 --- a/site/src/components/ActiveUserChart/ActiveUserChart.tsx +++ b/site/src/components/ActiveUserChart/ActiveUserChart.tsx @@ -42,7 +42,7 @@ ChartJS.register( const USER_LIMIT_DISPLAY_THRESHOLD = 60; export interface ActiveUserChartProps { - data: Array<{ date: string; amount: number }>; + data: readonly { date: string; amount: number }[]; interval: "day" | "week"; userLimit: number | undefined; } diff --git a/site/src/components/Timeline/Timeline.tsx b/site/src/components/Timeline/Timeline.tsx index 665f3a7c92..017a8b5335 100644 --- a/site/src/components/Timeline/Timeline.tsx +++ b/site/src/components/Timeline/Timeline.tsx @@ -4,7 +4,7 @@ import { TimelineDateRow } from "components/Timeline/TimelineDateRow"; type GetDateFn = (data: TData) => Date; const groupByDate = ( - items: TData[], + items: readonly TData[], getDate: GetDateFn, ): Record => { const itemsByDate: Record = {}; @@ -23,7 +23,7 @@ const groupByDate = ( }; export interface TimelineProps { - items: TData[]; + items: readonly TData[]; getDate: GetDateFn; row: (item: TData) => JSX.Element; } diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index f6c54b3641..ae6637238e 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -41,7 +41,7 @@ export interface ProxyContextValue { // WorkspaceProxy[] is returned if the user is an admin. WorkspaceProxy extends Region with // more information about the proxy and the status. More information includes the error message if // the proxy is unhealthy. - proxies?: Region[] | WorkspaceProxy[]; + proxies?: readonly Region[] | readonly WorkspaceProxy[]; // isFetched is true when the 'proxies' api call is complete. isFetched: boolean; isLoading: boolean; @@ -117,7 +117,7 @@ export const ProxyProvider: FC = ({ children }) => { }); const { permissions } = useAuthenticated(); - const query = async (): Promise => { + const query = async (): Promise => { const endpoint = permissions.editWorkspaceProxies ? getWorkspaceProxies : getWorkspaceProxyRegions; @@ -218,7 +218,7 @@ export const useProxy = (): ProxyContextValue => { * If not, `primary` is always the best default. */ export const getPreferredProxy = ( - proxies: Region[], + proxies: readonly Region[], selectedProxy?: Region, latencies?: Record, autoSelectBasedOnLatency = true, @@ -245,7 +245,7 @@ export const getPreferredProxy = ( }; const selectByLatency = ( - proxies: Region[], + proxies: readonly Region[], latencies?: Record, ): Region | undefined => { if (!latencies) { diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts index d50f7ead1d..9d28830b7d 100644 --- a/site/src/contexts/useProxyLatency.ts +++ b/site/src/contexts/useProxyLatency.ts @@ -37,7 +37,7 @@ const proxyLatenciesReducer = ( }; export const useProxyLatency = ( - proxies?: Region[], + proxies?: readonly Region[], ): { // Refetch can be called to refetch the proxy latencies. // Until the new values are loaded, the old values will still be used. @@ -265,7 +265,7 @@ const updateStoredLatencies = (action: ProxyLatencyAction): void => { // garbageCollectStoredLatencies will remove any latencies that are older then 1 week or latencies of proxies // that no longer exist. This is intended to keep the size of local storage down. const garbageCollectStoredLatencies = ( - regions: Region[], + regions: readonly Region[], maxStored: number, ): void => { const latencies = loadStoredLatencies(); @@ -282,7 +282,7 @@ const garbageCollectStoredLatencies = ( const cleanupLatencies = ( stored: Record, - regions: Region[], + regions: readonly Region[], now: Date, maxStored: number, ): Record => { diff --git a/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx b/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx index 7aab2ebdb0..d8867aee47 100644 --- a/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx +++ b/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx @@ -27,8 +27,8 @@ const styles = { } satisfies Record>; export interface LicenseBannerViewProps { - errors: string[]; - warnings: string[]; + errors: readonly string[]; + warnings: readonly string[]; } export const LicenseBannerView: FC = ({ diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 558706eee6..65f14e7c12 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -30,7 +30,7 @@ export interface NavbarViewProps { logo_url?: string; user?: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; - supportLinks?: TypesGen.LinkConfig[]; + supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; canViewAuditLog: boolean; canViewDeployment: boolean; @@ -342,57 +342,58 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { - {proxyContextValue.proxies - ?.sort((a, b) => { - const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity; - const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity; - return latencyA - latencyB; - }) - .map((proxy) => ( - { - if (!proxy.healthy) { - displayError("Please select a healthy workspace proxy."); - closeMenu(); - return; - } + {proxyContextValue.proxies && + [...proxyContextValue.proxies] + .sort((a, b) => { + const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity; + const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity; + return latencyA - latencyB; + }) + .map((proxy) => ( + { + if (!proxy.healthy) { + displayError("Please select a healthy workspace proxy."); + closeMenu(); + return; + } - proxyContextValue.setProxy(proxy); - closeMenu(); - }} - > -
-
- +
+ +
+ + {proxy.display_name} + +
- - {proxy.display_name} - - -
-
- ))} +
+ ))} diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx index 35f7cbeb0b..1ded0a640c 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -15,7 +15,7 @@ import { UserDropdownContent } from "./UserDropdownContent"; export interface UserDropdownProps { user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; - supportLinks?: TypesGen.LinkConfig[]; + supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; children?: ReactNode; } diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index bc6cc233d7..4cce813939 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -83,7 +83,7 @@ const styles = { export interface UserDropdownContentProps { user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; - supportLinks?: TypesGen.LinkConfig[]; + supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; } diff --git a/site/src/modules/resources/AgentLogs/AgentLogs.tsx b/site/src/modules/resources/AgentLogs/AgentLogs.tsx index d101c1d950..2216b7eae2 100644 --- a/site/src/modules/resources/AgentLogs/AgentLogs.tsx +++ b/site/src/modules/resources/AgentLogs/AgentLogs.tsx @@ -20,8 +20,8 @@ type AgentLogsProps = Omit< ComponentProps, "children" | "itemSize" | "itemCount" > & { - logs: LineWithID[]; - sources: WorkspaceAgentLogSource[]; + logs: readonly LineWithID[]; + sources: readonly WorkspaceAgentLogSource[]; }; export const AgentLogs = forwardRef( diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index e445f99ea1..7c0def4262 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -116,7 +116,7 @@ const getValidationSchema = (): Yup.AnyObjectSchema => }); interface PortForwardPopoverViewProps extends PortForwardButtonProps { - listeningPorts?: WorkspaceAgentListeningPort[]; + listeningPorts?: readonly WorkspaceAgentListeningPort[]; portSharingExperimentEnabled: boolean; portSharingControlsEnabled: boolean; } diff --git a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx index efbc9e32c6..73597dd22b 100644 --- a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx +++ b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx @@ -15,7 +15,7 @@ export interface VSCodeDesktopButtonProps { workspaceName: string; agentName?: string; folderPath?: string; - displayApps: DisplayApp[]; + displayApps: readonly DisplayApp[]; } type VSCodeVariant = "vscode" | "vscode-insiders"; diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index a6aa69128a..70c12fa1ca 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -32,7 +32,7 @@ export const Language = { }; export interface AuditPageViewProps { - auditLogs?: AuditLog[]; + auditLogs?: readonly AuditLog[]; isNonInitialPage: boolean; isAuditLogVisible: boolean; error?: unknown; diff --git a/site/src/pages/DeploySettingsPage/OptionsTable.tsx b/site/src/pages/DeploySettingsPage/OptionsTable.tsx index c83506e2c7..5f2dac3901 100644 --- a/site/src/pages/DeploySettingsPage/OptionsTable.tsx +++ b/site/src/pages/DeploySettingsPage/OptionsTable.tsx @@ -17,8 +17,8 @@ import { import { optionValue } from "./optionValue"; interface OptionsTableProps { - options: SerpentOption[]; - additionalValues?: string[]; + options: readonly SerpentOption[]; + additionalValues?: readonly string[]; } const OptionsTable: FC = ({ options, additionalValues }) => { diff --git a/site/src/pages/DeploySettingsPage/optionValue.ts b/site/src/pages/DeploySettingsPage/optionValue.ts index bfb06f8817..75435a894c 100644 --- a/site/src/pages/DeploySettingsPage/optionValue.ts +++ b/site/src/pages/DeploySettingsPage/optionValue.ts @@ -4,7 +4,7 @@ import type { SerpentOption } from "api/typesGenerated"; // optionValue is a helper function to format the value of a specific deployment options export function optionValue( option: SerpentOption, - additionalValues?: string[], + additionalValues?: readonly string[], ) { // If option annotations are present, use them to format the value. if (option.annotations) { diff --git a/site/src/pages/HealthPage/Content.tsx b/site/src/pages/HealthPage/Content.tsx index 20e4764151..28dcacdd72 100644 --- a/site/src/pages/HealthPage/Content.tsx +++ b/site/src/pages/HealthPage/Content.tsx @@ -214,7 +214,7 @@ export const BooleanPill: FC = ({ ); }; -type LogsProps = { lines: string[] } & HTMLAttributes; +type LogsProps = HTMLAttributes & { lines: readonly string[] }; export const Logs: FC = ({ lines, ...divProps }) => { const theme = useTheme(); diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index b20ec3f370..e067425a63 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -296,7 +296,7 @@ const UsersLatencyPanel: FC = ({ {!data && } {users && users.length === 0 && } {users && - users + [...users] .sort((a, b) => b.latency_ms.p50 - a.latency_ms.p50) .map((row) => (
= ({ {!data && } {users && users.length === 0 && } {users && - users + [...users] .sort((a, b) => b.seconds - a.seconds) .map((row) => (
= ({ }; interface TemplateUsagePanelProps extends PanelProps { - data: TemplateAppUsage[] | undefined; + data: readonly TemplateAppUsage[] | undefined; } const TemplateUsagePanel: FC = ({ @@ -508,7 +508,7 @@ const TemplateUsagePanel: FC = ({ }; interface TemplateParametersUsagePanelProps extends PanelProps { - data: TemplateParameterUsage[] | undefined; + data: readonly TemplateParameterUsage[] | undefined; } const TemplateParametersUsagePanel: FC = ({ @@ -579,7 +579,7 @@ const TemplateParametersUsagePanel: FC = ({
Count
- {parameter.values + {[...parameter.values] .sort((a, b) => b.count - a.count) .filter((usage) => filterOrphanValues(usage, parameter)) .map((usage, usageIndex) => ( diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx index 9c1998841d..fbd76ba2be 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx @@ -113,7 +113,7 @@ const ProxyMessagesRow: FC = ({ proxy }) => { interface ProxyMessagesListProps { title: ReactNode; - messages?: string[]; + messages?: readonly string[]; } const ProxyMessagesList: FC = ({ title, messages }) => { diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx index 4c45799f2b..3b4a9ebfa4 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx @@ -15,7 +15,7 @@ import type { ProxyLatencyReport } from "contexts/useProxyLatency"; import { ProxyRow } from "./WorkspaceProxyRow"; export interface WorkspaceProxyViewProps { - proxies?: Region[]; + proxies?: readonly Region[]; proxyLatencies?: Record; getWorkspaceProxiesError?: unknown; isLoading: boolean; diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index 54daa3c2a9..9349311392 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -9,7 +9,7 @@ import { UsersFilter } from "./UsersFilter"; import { UsersTable } from "./UsersTable/UsersTable"; export interface UsersPageViewProps { - users?: TypesGen.User[]; + users?: readonly TypesGen.User[]; roles?: TypesGen.AssignableRoles[]; isUpdatingUserRoles?: boolean; canEditUsers: boolean; diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx index 14075cc754..76099c9796 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx @@ -69,7 +69,7 @@ const Option: FC = ({ export interface EditRolesButtonProps { isLoading: boolean; - roles: Role[]; + roles: readonly Role[]; selectedRoleNames: Set; onChange: (roles: Role["name"][]) => void; isDefaultOpen?: boolean; diff --git a/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx b/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx index 5b95dc8e19..36090dbfde 100644 --- a/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx +++ b/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx @@ -160,7 +160,7 @@ const roleNamesByAccessLevel: readonly string[] = [ "auditor", ]; -function sortRolesByAccessLevel(roles: Role[]) { +function sortRolesByAccessLevel(roles: readonly Role[]): readonly Role[] { if (roles.length === 0) { return roles; } diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx index ddf7634364..119b00a851 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx @@ -21,7 +21,7 @@ export const Language = { } as const; export interface UsersTableProps { - users: TypesGen.User[] | undefined; + users: readonly TypesGen.User[] | undefined; roles: TypesGen.AssignableRoles[] | undefined; groupsByUserId: GroupsByUserId | undefined; isUpdatingUserRoles?: boolean; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index cb96cec2ff..03222edbed 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -36,7 +36,7 @@ import { UserRoleCell } from "./UserRoleCell"; dayjs.extend(relativeTime); interface UsersTableBodyProps { - users: TypesGen.User[] | undefined; + users: readonly TypesGen.User[] | undefined; groupsByUserId: GroupsByUserId | undefined; authMethods?: TypesGen.AuthMethods; roles?: TypesGen.AssignableRoles[]; diff --git a/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx index 3159ac5b0d..5358f5d3dc 100644 --- a/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx @@ -13,7 +13,7 @@ import { getResourceIconPath } from "utils/workspace"; dayjs.extend(relativeTime); type BatchDeleteConfirmationProps = { - checkedWorkspaces: Workspace[]; + checkedWorkspaces: readonly Workspace[]; open: boolean; isLoading: boolean; onClose: () => void; @@ -111,7 +111,7 @@ export const BatchDeleteConfirmation: FC = ({ }; interface StageProps { - workspaces: Workspace[]; + workspaces: readonly Workspace[]; } const Consequences: FC = () => { diff --git a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.stories.tsx b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.stories.tsx index b9a9861508..5a7143cb56 100644 --- a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.stories.tsx +++ b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.stories.tsx @@ -1,6 +1,7 @@ import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { useQueryClient } from "react-query"; +import type { Workspace } from "api/typesGenerated"; import { chromatic } from "testHelpers/chromatic"; import { MockWorkspace, @@ -29,23 +30,33 @@ const workspaces = [ }, ]; -const updates = new Map(); -for (const it of workspaces) { - const versionId = it.template_active_version_id; - const version = updates.get(versionId); +function getPopulatedUpdates(): Map { + type MutableUpdate = Omit & { + affected_workspaces: Workspace[]; + }; - if (version) { - version.affected_workspaces.push(it); - continue; + const updates = new Map(); + for (const it of workspaces) { + const versionId = it.template_active_version_id; + const version = updates.get(versionId); + + if (version) { + version.affected_workspaces.push(it); + continue; + } + + updates.set(versionId, { + ...MockTemplateVersion, + template_display_name: it.template_display_name, + affected_workspaces: [it], + }); } - updates.set(versionId, { - ...MockTemplateVersion, - template_display_name: it.template_display_name, - affected_workspaces: [it], - }); + return updates as Map; } +const updates = getPopulatedUpdates(); + const meta: Meta = { title: "pages/WorkspacesPage/BatchUpdateConfirmation", parameters: { chromatic }, diff --git a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx index fd7ff519de..de19212bcb 100644 --- a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx @@ -18,7 +18,7 @@ import { Stack } from "components/Stack/Stack"; dayjs.extend(relativeTime); type BatchUpdateConfirmationProps = { - checkedWorkspaces: Workspace[]; + checkedWorkspaces: readonly Workspace[]; open: boolean; isLoading: boolean; onClose: () => void; @@ -27,7 +27,7 @@ type BatchUpdateConfirmationProps = { export interface Update extends TemplateVersion { template_display_name: string; - affected_workspaces: Workspace[]; + affected_workspaces: readonly Workspace[]; } export const BatchUpdateConfirmation: FC = ({ @@ -90,11 +90,13 @@ export const BatchUpdateConfirmation: FC = ({ // Figure out which new versions everything will be updated to so that we can // show update messages and such. const newVersions = useMemo(() => { - const newVersions = new Map< - string, - Pick - >(); + type MutableUpdateInfo = { + id: string; + template_display_name: string; + affected_workspaces: Workspace[]; + }; + const newVersions = new Map(); for (const it of workspacesToUpdate) { const versionId = it.template_active_version_id; const version = newVersions.get(versionId); @@ -111,7 +113,11 @@ export const BatchUpdateConfirmation: FC = ({ }); } - return newVersions; + type ReadonlyUpdateInfo = Readonly & { + affected_workspaces: readonly Workspace[]; + }; + + return newVersions as Map; }, [workspacesToUpdate]); // Not all of the information we want is included in the `Workspace` type, so we @@ -401,7 +407,7 @@ const TemplateVersionMessages: FC = ({ }; interface UsedByProps { - workspaces: Workspace[]; + workspaces: readonly Workspace[]; } const UsedBy: FC = ({ workspaces }) => { diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index be9e05bec5..bf959073aa 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -54,7 +54,9 @@ const WorkspacesPage: FC = () => { }); const updateWorkspace = useWorkspaceUpdate(queryKey); - const [checkedWorkspaces, setCheckedWorkspaces] = useState([]); + const [checkedWorkspaces, setCheckedWorkspaces] = useState< + readonly Workspace[] + >([]); const [confirmingBatchAction, setConfirmingBatchAction] = useState< "delete" | "update" | null >(null); diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 4035202882..48f78e71f2 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -44,15 +44,15 @@ type TemplateQuery = UseQueryResult; export interface WorkspacesPageViewProps { error: unknown; - workspaces?: Workspace[]; - checkedWorkspaces: Workspace[]; + workspaces?: readonly Workspace[]; + checkedWorkspaces: readonly Workspace[]; count?: number; filterProps: ComponentProps; page: number; limit: number; onPageChange: (page: number) => void; onUpdateWorkspace: (workspace: Workspace) => void; - onCheckChange: (checkedWorkspaces: Workspace[]) => void; + onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void; isRunningBatchAction: boolean; onDeleteAll: () => void; onUpdateAll: () => void; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 6e166dce57..4eb8c686f3 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -30,12 +30,12 @@ import { getDisplayWorkspaceTemplateName } from "utils/workspace"; import { WorkspacesEmpty } from "./WorkspacesEmpty"; export interface WorkspacesTableProps { - workspaces?: Workspace[]; - checkedWorkspaces: Workspace[]; + workspaces?: readonly Workspace[]; + checkedWorkspaces: readonly Workspace[]; error?: unknown; isUsingFilter: boolean; onUpdateWorkspace: (workspace: Workspace) => void; - onCheckChange: (checkedWorkspaces: Workspace[]) => void; + onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void; canCheckWorkspaces: boolean; templates?: Template[]; canCreateTemplate: boolean; diff --git a/site/src/pages/WorkspacesPage/batchActions.tsx b/site/src/pages/WorkspacesPage/batchActions.tsx index 04ae8817b7..a9e3eb1cf4 100644 --- a/site/src/pages/WorkspacesPage/batchActions.tsx +++ b/site/src/pages/WorkspacesPage/batchActions.tsx @@ -18,7 +18,7 @@ export function useBatchActions(options: UseBatchActionsProps) { const { onSuccess } = options; const startAllMutation = useMutation({ - mutationFn: (workspaces: Workspace[]) => { + mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all( workspaces.map((w) => startWorkspace(w.id, w.latest_build.template_version_id), @@ -32,7 +32,7 @@ export function useBatchActions(options: UseBatchActionsProps) { }); const stopAllMutation = useMutation({ - mutationFn: (workspaces: Workspace[]) => { + mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all(workspaces.map((w) => stopWorkspace(w.id))); }, onSuccess, @@ -42,7 +42,7 @@ export function useBatchActions(options: UseBatchActionsProps) { }); const deleteAllMutation = useMutation({ - mutationFn: (workspaces: Workspace[]) => { + mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all(workspaces.map((w) => deleteWorkspace(w.id))); }, onSuccess, @@ -52,7 +52,7 @@ export function useBatchActions(options: UseBatchActionsProps) { }); const updateAllMutation = useMutation({ - mutationFn: (workspaces: Workspace[]) => { + mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all( workspaces .filter((w) => w.outdated && !w.dormant_at) @@ -66,7 +66,7 @@ export function useBatchActions(options: UseBatchActionsProps) { }); const favoriteAllMutation = useMutation({ - mutationFn: (workspaces: Workspace[]) => { + mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all( workspaces .filter((w) => !w.favorite) @@ -80,7 +80,7 @@ export function useBatchActions(options: UseBatchActionsProps) { }); const unfavoriteAllMutation = useMutation({ - mutationFn: (workspaces: Workspace[]) => { + mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all( workspaces .filter((w) => w.favorite)