diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a446420fc31b9..fcea8168d0000 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3000,6 +3000,7 @@ const docTemplate = `{ ], "summary": "Get template examples by organization", "operationId": "get-template-examples-by-organization", + "deprecated": true, "parameters": [ { "type": "string", @@ -3421,6 +3422,34 @@ const docTemplate = `{ } } }, + "/templates/examples": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Get template examples", + "operationId": "get-template-examples", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.TemplateExample" + } + } + } + } + } + }, "/templates/{template}": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index af6106bf23153..f519081f6d425 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2630,6 +2630,7 @@ "tags": ["Templates"], "summary": "Get template examples by organization", "operationId": "get-template-examples-by-organization", + "deprecated": true, "parameters": [ { "type": "string", @@ -3005,6 +3006,30 @@ } } }, + "/templates/examples": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Get template examples", + "operationId": "get-template-examples", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.TemplateExample" + } + } + } + } + } + }, "/templates/{template}": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 043176279194c..896918f1c6d76 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -871,7 +871,7 @@ func New(options *Options) *API { r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) r.Get("/", api.templatesByOrganization()) - r.Get("/examples", api.templateExamples) + r.Get("/examples", api.templateExamplesByOrganization) r.Route("/{templatename}", func(r chi.Router) { r.Get("/", api.templateByOrganizationAndName) r.Route("/versions/{templateversionname}", func(r chi.Router) { @@ -915,6 +915,7 @@ func New(options *Options) *API { apiKeyMiddleware, ) r.Get("/", api.fetchTemplates(nil)) + r.Get("/examples", api.templateExamples) r.Route("/{template}", func(r chi.Router) { r.Use( httpmw.ExtractTemplateParam(options.Database), diff --git a/coderd/templates.go b/coderd/templates.go index 5bf32871dcbc1..93a5943b40193 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -821,7 +821,8 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { // @Param organization path string true "Organization ID" format(uuid) // @Success 200 {array} codersdk.TemplateExample // @Router /organizations/{organization}/templates/examples [get] -func (api *API) templateExamples(rw http.ResponseWriter, r *http.Request) { +// @Deprecated Use /templates/examples instead +func (api *API) templateExamplesByOrganization(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() organization = httpmw.OrganizationParam(r) @@ -844,6 +845,33 @@ func (api *API) templateExamples(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, ex) } +// @Summary Get template examples +// @ID get-template-examples +// @Security CoderSessionToken +// @Produce json +// @Tags Templates +// @Success 200 {array} codersdk.TemplateExample +// @Router /templates/examples [get] +func (api *API) templateExamples(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if !api.Authorize(r, policy.ActionRead, rbac.ResourceTemplate.AnyOrganization()) { + httpapi.ResourceNotFound(rw) + return + } + + ex, err := examples.List() + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching examples.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, ex) +} + func (api *API) convertTemplates(templates []database.Template) []codersdk.Template { apiTemplates := make([]codersdk.Template, 0, len(templates)) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index cd54bfdaeaba7..a03a1c619871e 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -1097,17 +1097,17 @@ func TestPreviousTemplateVersion(t *testing.T) { }) } -func TestTemplateExamples(t *testing.T) { +func TestStarterTemplates(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - ex, err := client.TemplateExamples(ctx, user.OrganizationID) + ex, err := client.StarterTemplates(ctx) require.NoError(t, err) ls, err := examples.List() require.NoError(t, err) diff --git a/codersdk/templates.go b/codersdk/templates.go index cad6ef2ca49dc..56b3b3c0483ef 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -472,9 +472,16 @@ type AgentStatsReportResponse struct { TxBytes int64 `json:"tx_bytes"` } -// TemplateExamples lists example templates embedded in coder. -func (c *Client) TemplateExamples(ctx context.Context, organizationID uuid.UUID) ([]TemplateExample, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/templates/examples", organizationID), nil) +// TemplateExamples lists example templates available in Coder. +// +// Deprecated: Use StarterTemplates instead. +func (c *Client) TemplateExamples(ctx context.Context, _ uuid.UUID) ([]TemplateExample, error) { + return c.StarterTemplates(ctx) +} + +// StarterTemplates lists example templates available in Coder. +func (c *Client) StarterTemplates(ctx context.Context) ([]TemplateExample, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/templates/examples", nil) if err != nil { return nil, err } diff --git a/docs/api/templates.md b/docs/api/templates.md index f42c4306d01a8..1a47cb600096a 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -761,6 +761,60 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get template examples + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/templates/examples \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /templates/examples` + +### Example responses + +> 200 Response + +```json +[ + { + "description": "string", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "markdown": "string", + "name": "string", + "tags": ["string"], + "url": "string" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.TemplateExample](schemas.md#codersdktemplateexample) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| --------------- | ------------ | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | +| `» description` | string | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» markdown` | string | false | | | +| `» name` | string | false | | | +| `» tags` | array | false | | | +| `» url` | string | false | | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get template metadata by ID ### Code samples diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8dcef31bf676e..e8d7e1c8c7be5 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -483,7 +483,7 @@ class ApiMethods { }; deleteToken = async (keyId: string): Promise => { - await this.axios.delete("/api/v2/users/me/keys/" + keyId); + await this.axios.delete(`/api/v2/users/me/keys/${keyId}`); }; createToken = async ( @@ -1754,12 +1754,8 @@ class ApiMethods { /** * @param organization Can be the organization's ID or name */ - getTemplateExamples = async ( - organization: string, - ): Promise => { - const response = await this.axios.get( - `/api/v2/organizations/${organization}/templates/examples`, - ); + getTemplateExamples = async (): Promise => { + const response = await this.axios.get(`/api/v2/templates/examples`); return response.data; }; diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index d7a03dd4279ff..c6d0f0ac74c4d 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -112,13 +112,10 @@ export const setGroupRole = ( }; }; -export const templateExamples = (organizationId: string) => { +export const templateExamples = () => { return { - queryKey: [ - ...getTemplatesByOrganizationQueryKey(organizationId), - "examples", - ], - queryFn: () => API.getTemplateExamples(organizationId), + queryKey: ["templates", "examples"], + queryFn: () => API.getTemplateExamples(), }; }; diff --git a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx index 922ee6ae11b2b..6d9da12f5aef2 100644 --- a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx @@ -31,7 +31,7 @@ export const ImportStarterTemplateView: FC = ({ const { multiple_organizations: organizationsEnabled } = useFeatureVisibility(); const [searchParams] = useSearchParams(); - const templateExamplesQuery = useQuery(templateExamples("default")); + const templateExamplesQuery = useQuery(templateExamples()); const templateExample = templateExamplesQuery.data?.find( (e) => e.id === searchParams.get("exampleId")!, ); diff --git a/site/src/pages/CreateTemplatesGalleryPage/CreateTemplatesGalleryPage.tsx b/site/src/pages/CreateTemplatesGalleryPage/CreateTemplatesGalleryPage.tsx index e7cae2724486d..0f0a9457637ed 100644 --- a/site/src/pages/CreateTemplatesGalleryPage/CreateTemplatesGalleryPage.tsx +++ b/site/src/pages/CreateTemplatesGalleryPage/CreateTemplatesGalleryPage.tsx @@ -11,7 +11,7 @@ import { StarterTemplatesPageView } from "./StarterTemplatesPageView"; const CreateTemplatesGalleryPage: FC = () => { const { experiments } = useDashboard(); - const templateExamplesQuery = useQuery(templateExamples("default")); + const templateExamplesQuery = useQuery(templateExamples()); const starterTemplatesByTag = templateExamplesQuery.data ? // Currently, the scratch template should not be displayed on the starter templates page. getTemplatesByTag(removeScratchExample(templateExamplesQuery.data)) diff --git a/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx b/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx index df042a7cca743..0c4c3b5c8b492 100644 --- a/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx +++ b/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx @@ -8,7 +8,7 @@ import { StarterTemplatePageView } from "./StarterTemplatePageView"; const StarterTemplatePage: FC = () => { const { exampleId } = useParams() as { exampleId: string }; - const templateExamplesQuery = useQuery(templateExamples("default")); + const templateExamplesQuery = useQuery(templateExamples()); const starterTemplate = templateExamplesQuery.data?.find( (example) => example.id === exampleId, ); diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index dbeab83c21cd4..fa9c5bb167669 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -11,7 +11,7 @@ export const TemplatesPage: FC = () => { const templatesQuery = useQuery(templates()); const examplesQuery = useQuery({ - ...templateExamples("default"), + ...templateExamples(), enabled: permissions.createTemplates, }); const error = templatesQuery.error || examplesQuery.error; diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 1d53a8e9aa975..55830c093b71a 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -47,9 +47,6 @@ export const handlers = [ http.get("/api/v2/organizations/:organizationId", () => { return HttpResponse.json(M.MockOrganization); }), - http.get("/api/v2/organizations/:organizationId/templates/examples", () => { - return HttpResponse.json([M.MockTemplateExample, M.MockTemplateExample2]); - }), http.get( "/api/v2/organizations/:organizationId/templates/:templateId", () => { @@ -81,6 +78,9 @@ export const handlers = [ ), // templates + http.get("/api/v2/templates/examples", () => { + return HttpResponse.json([M.MockTemplateExample, M.MockTemplateExample2]); + }), http.get("/api/v2/templates/:templateId", () => { return HttpResponse.json(M.MockTemplate); }),