Skip to content

Commit f262fb4

Browse files
feat: Add template version page (#5071)
1 parent 773fc73 commit f262fb4

34 files changed

+1769
-843
lines changed

coderd/coderd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,10 @@ func New(options *Options) *API {
340340
httpmw.ExtractOrganizationParam(options.Database),
341341
)
342342
r.Get("/", api.organization)
343-
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
343+
r.Route("/templateversions", func(r chi.Router) {
344+
r.Post("/", api.postTemplateVersionsByOrganization)
345+
r.Get("/{templateversionname}", api.templateVersionByOrganizationAndName)
346+
})
344347
r.Route("/templates", func(r chi.Router) {
345348
r.Post("/", api.postTemplateByOrganization)
346349
r.Get("/", api.templatesByOrganization)

coderd/coderdtest/authorize.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,11 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
238238
"GET:/api/v2/applications/auth-redirect": {AssertAction: rbac.ActionCreate, AssertObject: rbac.ResourceAPIKey},
239239

240240
// These endpoints need payloads to get to the auth part. Payloads will be required
241-
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
242-
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
243-
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
244-
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
241+
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
242+
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
243+
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
244+
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
245+
"GET:/api/v2/organizations/{organization}/templateversions/{templateversionname}": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
245246

246247
// Endpoints that use the SQLQuery filter.
247248
"GET:/api/v2/workspaces/": {StatusCode: http.StatusOK, NoAuthorize: true},

coderd/database/databasefake/databasefake.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,22 @@ func (q *fakeQuerier) GetTemplateVersionByTemplateIDAndName(_ context.Context, a
14711471
return database.TemplateVersion{}, sql.ErrNoRows
14721472
}
14731473

1474+
func (q *fakeQuerier) GetTemplateVersionByOrganizationAndName(_ context.Context, arg database.GetTemplateVersionByOrganizationAndNameParams) (database.TemplateVersion, error) {
1475+
q.mutex.RLock()
1476+
defer q.mutex.RUnlock()
1477+
1478+
for _, templateVersion := range q.templateVersions {
1479+
if templateVersion.OrganizationID != arg.OrganizationID {
1480+
continue
1481+
}
1482+
if !strings.EqualFold(templateVersion.Name, arg.Name) {
1483+
continue
1484+
}
1485+
return templateVersion, nil
1486+
}
1487+
return database.TemplateVersion{}, sql.ErrNoRows
1488+
}
1489+
14741490
func (q *fakeQuerier) GetTemplateVersionByID(_ context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) {
14751491
q.mutex.RLock()
14761492
defer q.mutex.RUnlock()

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templateversions.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ WHERE
5252
template_id = $1
5353
AND "name" = $2;
5454

55+
-- name: GetTemplateVersionByOrganizationAndName :one
56+
SELECT
57+
*
58+
FROM
59+
template_versions
60+
WHERE
61+
organization_id = $1
62+
AND "name" = $2;
63+
5564
-- name: GetTemplateVersionByID :one
5665
SELECT
5766
*

coderd/templateversions.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,48 @@ func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
596596
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job), user))
597597
}
598598

599+
func (api *API) templateVersionByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
600+
ctx := r.Context()
601+
organization := httpmw.OrganizationParam(r)
602+
templateVersionName := chi.URLParam(r, "templateversionname")
603+
templateVersion, err := api.Database.GetTemplateVersionByOrganizationAndName(ctx, database.GetTemplateVersionByOrganizationAndNameParams{
604+
OrganizationID: organization.ID,
605+
Name: templateVersionName,
606+
})
607+
if errors.Is(err, sql.ErrNoRows) {
608+
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
609+
Message: fmt.Sprintf("No template version found by name %q.", templateVersionName),
610+
})
611+
return
612+
}
613+
if err != nil {
614+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
615+
Message: "Internal error fetching template version.",
616+
Detail: err.Error(),
617+
})
618+
return
619+
}
620+
job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
621+
if err != nil {
622+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
623+
Message: "Internal error fetching provisioner job.",
624+
Detail: err.Error(),
625+
})
626+
return
627+
}
628+
629+
user, err := api.Database.GetUserByID(ctx, templateVersion.CreatedBy)
630+
if err != nil {
631+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
632+
Message: "Internal error on fetching user.",
633+
Detail: err.Error(),
634+
})
635+
return
636+
}
637+
638+
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job), user))
639+
}
640+
599641
func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) {
600642
var (
601643
ctx = r.Context()

coderd/templateversions_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,3 +928,36 @@ func TestPaginatedTemplateVersions(t *testing.T) {
928928
})
929929
}
930930
}
931+
932+
func TestTemplateVersionByOrganizationAndName(t *testing.T) {
933+
t.Parallel()
934+
t.Run("NotFound", func(t *testing.T) {
935+
t.Parallel()
936+
client := coderdtest.New(t, nil)
937+
user := coderdtest.CreateFirstUser(t, client)
938+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
939+
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
940+
941+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
942+
defer cancel()
943+
944+
_, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, "nothing")
945+
var apiErr *codersdk.Error
946+
require.ErrorAs(t, err, &apiErr)
947+
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
948+
})
949+
950+
t.Run("Found", func(t *testing.T) {
951+
t.Parallel()
952+
client := coderdtest.New(t, nil)
953+
user := coderdtest.CreateFirstUser(t, client)
954+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
955+
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
956+
957+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
958+
defer cancel()
959+
960+
_, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, version.Name)
961+
require.NoError(t, err)
962+
})
963+
}

codersdk/organizations.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,25 @@ func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.
138138
return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion)
139139
}
140140

141+
func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organizationID uuid.UUID, name string) (TemplateVersion, error) {
142+
res, err := c.Request(ctx, http.MethodGet,
143+
fmt.Sprintf("/api/v2/organizations/%s/templateversions/%s", organizationID.String(), name),
144+
nil,
145+
)
146+
147+
if err != nil {
148+
return TemplateVersion{}, xerrors.Errorf("execute request: %w", err)
149+
}
150+
defer res.Body.Close()
151+
152+
if res.StatusCode != http.StatusOK {
153+
return TemplateVersion{}, readBodyAsError(res)
154+
}
155+
156+
var templateVersion TemplateVersion
157+
return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion)
158+
}
159+
141160
// CreateTemplate creates a new template inside an organization.
142161
func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, request CreateTemplateRequest) (Template, error) {
143162
res, err := c.Request(ctx, http.MethodPost,

site/js-untar.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
declare module "js-untar" {
2+
interface File {
3+
name: string
4+
readAsString: () => string
5+
}
6+
7+
const Untar: (buffer: ArrayBuffer) => {
8+
then: (
9+
resolve?: () => Promise<void>,
10+
reject?: () => Promise<void>,
11+
progress: (file: File) => Promise<void>,
12+
) => Promise<void>
13+
}
14+
15+
export default Untar
16+
}

site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"front-matter": "4.0.2",
5151
"history": "5.3.0",
5252
"i18next": "21.9.1",
53+
"js-untar": "2.0.0",
5354
"just-debounce-it": "3.1.1",
5455
"react": "18.2.0",
5556
"react-chartjs-2": "4.3.1",

site/src/AppRouter.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ const NetworkSettingsPage = lazy(
8383
() => import("./pages/DeploySettingsPage/NetworkSettingsPage"),
8484
)
8585
const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"))
86+
const TemplateVersionPage = lazy(
87+
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
88+
)
8689

8790
export const AppRouter: FC = () => {
8891
const xServices = useContext(XServiceContext)
@@ -123,16 +126,14 @@ export const AppRouter: FC = () => {
123126
}
124127
/>
125128

126-
<Route path="workspaces">
127-
<Route
128-
index
129-
element={
130-
<AuthAndFrame>
131-
<WorkspacesPage />
132-
</AuthAndFrame>
133-
}
134-
/>
135-
</Route>
129+
<Route
130+
path="workspaces"
131+
element={
132+
<AuthAndFrame>
133+
<WorkspacesPage />
134+
</AuthAndFrame>
135+
}
136+
/>
136137

137138
<Route path="templates">
138139
<Route
@@ -181,6 +182,16 @@ export const AppRouter: FC = () => {
181182
</RequireAuth>
182183
}
183184
/>
185+
<Route path="versions">
186+
<Route
187+
path=":version"
188+
element={
189+
<AuthAndFrame>
190+
<TemplateVersionPage />
191+
</AuthAndFrame>
192+
}
193+
/>
194+
</Route>
184195
</Route>
185196
</Route>
186197

site/src/__mocks__/js-untar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default jest.fn()

site/src/api/api.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@ export const getTemplateVersions = async (
219219
return response.data
220220
}
221221

222+
export const getTemplateVersionByName = async (
223+
organizationId: string,
224+
versionName: string,
225+
): Promise<TypesGen.TemplateVersion> => {
226+
const response = await axios.get<TypesGen.TemplateVersion>(
227+
`/api/v2/organizations/${organizationId}/templateversions/${versionName}`,
228+
)
229+
return response.data
230+
}
231+
222232
export const updateTemplateMeta = async (
223233
templateId: string,
224234
data: TypesGen.UpdateTemplateMeta,
@@ -646,3 +656,10 @@ export const getReplicas = async (): Promise<TypesGen.Replica[]> => {
646656
const response = await axios.get(`/api/v2/replicas`)
647657
return response.data
648658
}
659+
660+
export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
661+
const response = await axios.get<ArrayBuffer>(`/api/v2/files/${fileId}`, {
662+
responseType: "arraybuffer",
663+
})
664+
return response.data
665+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
2+
3+
export const MarkdownIcon = (props: SvgIconProps): JSX.Element => (
4+
<SvgIcon {...props} viewBox="0 0 32 32">
5+
<rect
6+
x="2.5"
7+
y="7.955"
8+
width="27"
9+
height="16.091"
10+
style={{ fill: "none", stroke: "#755838" }}
11+
/>
12+
<polygon
13+
points="5.909 20.636 5.909 11.364 8.636 11.364 11.364 14.773 14.091 11.364 16.818 11.364 16.818 20.636 14.091 20.636 14.091 15.318 11.364 18.727 8.636 15.318 8.636 20.636 5.909 20.636"
14+
style={{ stroke: "#755838" }}
15+
/>
16+
<polygon
17+
points="22.955 20.636 18.864 16.136 21.591 16.136 21.591 11.364 24.318 11.364 24.318 16.136 27.045 16.136 22.955 20.636"
18+
style={{ stroke: "#755838" }}
19+
/>
20+
</SvgIcon>
21+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
2+
3+
export const TerraformIcon = (props: SvgIconProps): JSX.Element => (
4+
<SvgIcon {...props} viewBox="0 0 32 32">
5+
<polygon
6+
points="12.042 6.858 20.071 11.448 20.071 20.462 12.042 15.868 12.042 6.858 12.042 6.858"
7+
style={{ fill: "#813cf3" }}
8+
/>
9+
<polygon
10+
points="20.5 20.415 28.459 15.84 28.459 6.887 20.5 11.429 20.5 20.415 20.5 20.415"
11+
style={{ fill: "#813cf3" }}
12+
/>
13+
<polygon
14+
points="3.541 11.01 11.571 15.599 11.571 6.59 3.541 2 3.541 11.01 3.541 11.01"
15+
style={{ fill: "#813cf3" }}
16+
/>
17+
<polygon
18+
points="12.042 25.41 20.071 30 20.071 20.957 12.042 16.368 12.042 25.41 12.042 25.41"
19+
style={{ fill: "#813cf3" }}
20+
/>
21+
</SvgIcon>
22+
)

0 commit comments

Comments
 (0)