mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: hide "Create Workspace" button for deleted templates (#22092)
**Background** Reported in #17417, there is a `deleted` query parameter supported by /api/v2/templates, but we do not respect this field on the client, showing the "Create Workspace" button for deleted templates. **Expected Behavior** Don't show the "Create Workspace" button for deleted templates. **Notes** This PR adds a new `deleted` field to the templates API response. Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
This commit is contained in:
Generated
+3
@@ -18919,6 +18919,9 @@ const docTemplate = `{
|
||||
"default_ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"deleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -17306,6 +17306,9 @@
|
||||
"default_ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"deleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -1131,6 +1131,7 @@ func (api *API) convertTemplate(
|
||||
RequireActiveVersion: templateAccessControl.RequireActiveVersion,
|
||||
Deprecated: templateAccessControl.IsDeprecated(),
|
||||
DeprecationMessage: templateAccessControl.Deprecated,
|
||||
Deleted: template.Deleted,
|
||||
MaxPortShareLevel: maxPortShareLevel,
|
||||
UseClassicParameterFlow: template.UseClassicParameterFlow,
|
||||
CORSBehavior: codersdk.CORSBehavior(template.CorsBehavior),
|
||||
|
||||
@@ -1801,6 +1801,49 @@ func TestDeleteTemplate(t *testing.T) {
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("DeletedIsSet", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Verify the deleted field is exposed in the SDK and set to false for active templates
|
||||
got, err := client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, got.Deleted)
|
||||
})
|
||||
|
||||
t.Run("DeletedIsTrue", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
err := client.DeleteTemplate(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the deleted field is set to true by listing templates with
|
||||
// deleted:true filter.
|
||||
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
|
||||
OrganizationID: user.OrganizationID,
|
||||
SearchQuery: "deleted:true",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, templates, 1)
|
||||
require.Equal(t, template.ID, templates[0].ID)
|
||||
require.True(t, templates[0].Deleted)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateMetrics(t *testing.T) {
|
||||
|
||||
@@ -32,6 +32,7 @@ type Template struct {
|
||||
Description string `json:"description"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
DeprecationMessage string `json:"deprecation_message"`
|
||||
Deleted bool `json:"deleted"`
|
||||
Icon string `json:"icon"`
|
||||
DefaultTTLMillis int64 `json:"default_ttl_ms"`
|
||||
ActivityBumpMillis int64 `json:"activity_bump_ms"`
|
||||
|
||||
Generated
+2
@@ -8443,6 +8443,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||
"created_by_name": "string",
|
||||
"default_ttl_ms": 0,
|
||||
"deleted": true,
|
||||
"deprecated": true,
|
||||
"deprecation_message": "string",
|
||||
"description": "string",
|
||||
@@ -8484,6 +8485,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
| `created_by_id` | string | false | | |
|
||||
| `created_by_name` | string | false | | |
|
||||
| `default_ttl_ms` | integer | false | | |
|
||||
| `deleted` | boolean | false | | |
|
||||
| `deprecated` | boolean | false | | |
|
||||
| `deprecation_message` | string | false | | |
|
||||
| `description` | string | false | | |
|
||||
|
||||
Generated
+8
@@ -62,6 +62,7 @@ To include deprecated templates, specify `deprecated:true` in the search query.
|
||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||
"created_by_name": "string",
|
||||
"default_ttl_ms": 0,
|
||||
"deleted": true,
|
||||
"deprecated": true,
|
||||
"deprecation_message": "string",
|
||||
"description": "string",
|
||||
@@ -120,6 +121,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
|`» created_by_id`|string(uuid)|false|||
|
||||
|`» created_by_name`|string|false|||
|
||||
|`» default_ttl_ms`|integer|false|||
|
||||
|`» deleted`|boolean|false|||
|
||||
|`» deprecated`|boolean|false|||
|
||||
|`» deprecation_message`|string|false|||
|
||||
|`» description`|string|false|||
|
||||
@@ -246,6 +248,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||
"created_by_name": "string",
|
||||
"default_ttl_ms": 0,
|
||||
"deleted": true,
|
||||
"deprecated": true,
|
||||
"deprecation_message": "string",
|
||||
"description": "string",
|
||||
@@ -397,6 +400,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||
"created_by_name": "string",
|
||||
"default_ttl_ms": 0,
|
||||
"deleted": true,
|
||||
"deprecated": true,
|
||||
"deprecation_message": "string",
|
||||
"description": "string",
|
||||
@@ -814,6 +818,7 @@ To include deprecated templates, specify `deprecated:true` in the search query.
|
||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||
"created_by_name": "string",
|
||||
"default_ttl_ms": 0,
|
||||
"deleted": true,
|
||||
"deprecated": true,
|
||||
"deprecation_message": "string",
|
||||
"description": "string",
|
||||
@@ -872,6 +877,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
|`» created_by_id`|string(uuid)|false|||
|
||||
|`» created_by_name`|string|false|||
|
||||
|`» default_ttl_ms`|integer|false|||
|
||||
|`» deleted`|boolean|false|||
|
||||
|`» deprecated`|boolean|false|||
|
||||
|`» deprecation_message`|string|false|||
|
||||
|`» description`|string|false|||
|
||||
@@ -1016,6 +1022,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
|
||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||
"created_by_name": "string",
|
||||
"default_ttl_ms": 0,
|
||||
"deleted": true,
|
||||
"deprecated": true,
|
||||
"deprecation_message": "string",
|
||||
"description": "string",
|
||||
@@ -1189,6 +1196,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
|
||||
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
|
||||
"created_by_name": "string",
|
||||
"default_ttl_ms": 0,
|
||||
"deleted": true,
|
||||
"deprecated": true,
|
||||
"deprecation_message": "string",
|
||||
"description": "string",
|
||||
|
||||
Generated
+1
@@ -5155,6 +5155,7 @@ export interface Template {
|
||||
readonly description: string;
|
||||
readonly deprecated: boolean;
|
||||
readonly deprecation_message: string;
|
||||
readonly deleted: boolean;
|
||||
readonly icon: string;
|
||||
readonly default_ttl_ms: number;
|
||||
readonly activity_bump_ms: number;
|
||||
|
||||
@@ -82,6 +82,13 @@ export const WithTemplates: Story = {
|
||||
display_name: "Deprecated",
|
||||
description: "Template is incompatible",
|
||||
},
|
||||
{
|
||||
...MockTemplate,
|
||||
name: "deleted-template",
|
||||
display_name: "Deleted",
|
||||
description: "Template has been deleted",
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
examples: [],
|
||||
workspacePermissions: {
|
||||
|
||||
@@ -85,6 +85,49 @@ const TemplateHelpTooltip: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
interface TemplateActionsProps {
|
||||
template: Template;
|
||||
workspacePermissions: Record<string, WorkspacePermissions> | undefined;
|
||||
templatePageLink: string;
|
||||
}
|
||||
|
||||
const TemplateActions: FC<TemplateActionsProps> = ({
|
||||
template,
|
||||
workspacePermissions,
|
||||
templatePageLink,
|
||||
}) => {
|
||||
if (template.deleted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (template.deprecated) {
|
||||
return <DeprecatedBadge />;
|
||||
}
|
||||
|
||||
if (
|
||||
!workspacePermissions?.[template.organization_id]?.createWorkspaceForUserID
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title={`Create a workspace using the ${template.display_name} template`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<RouterLink to={`${templatePageLink}/workspace`}>
|
||||
<ArrowRightIcon />
|
||||
Create Workspace
|
||||
</RouterLink>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface TemplateRowProps {
|
||||
showOrganizations: boolean;
|
||||
template: Template;
|
||||
@@ -149,25 +192,11 @@ const TemplateRow: FC<TemplateRowProps> = ({
|
||||
</TableCell>
|
||||
|
||||
<TableCell css={styles.actionCell}>
|
||||
{template.deprecated ? (
|
||||
<DeprecatedBadge />
|
||||
) : workspacePermissions?.[template.organization_id]
|
||||
?.createWorkspaceForUserID ? (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title={`Create a workspace using the ${template.display_name} template`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<RouterLink to={`${templatePageLink}/workspace`}>
|
||||
<ArrowRightIcon />
|
||||
Create Workspace
|
||||
</RouterLink>
|
||||
</Button>
|
||||
) : null}
|
||||
<TemplateActions
|
||||
template={template}
|
||||
workspacePermissions={workspacePermissions}
|
||||
templatePageLink={templatePageLink}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -852,6 +852,7 @@ export const MockTemplate: TypesGen.Template = {
|
||||
require_active_version: false,
|
||||
deprecated: false,
|
||||
deprecation_message: "",
|
||||
deleted: false,
|
||||
max_port_share_level: "public",
|
||||
use_classic_parameter_flow: false,
|
||||
cors_behavior: "simple",
|
||||
|
||||
Reference in New Issue
Block a user