diff --git a/codersdk/organizations.go b/codersdk/organizations.go index ce0690c466203..ca85dfbbc139e 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -24,14 +24,14 @@ type Organization struct { // CreateTemplateVersionRequest enables callers to create a new Template Version. type CreateTemplateVersionRequest struct { // TemplateID optionally associates a version with a template. - TemplateID uuid.UUID `json:"template_id"` + TemplateID uuid.UUID `json:"template_id,omitempty"` StorageMethod database.ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"` StorageSource string `json:"storage_source" validate:"required"` Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"` // ParameterValues allows for additional parameters to be provided // during the dry-run provision stage. - ParameterValues []CreateParameterRequest `json:"parameter_values"` + ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"` } // CreateTemplateRequest provides options when creating a template. @@ -45,7 +45,7 @@ type CreateTemplateRequest struct { // template works. There is no reason the data-model cannot support // empty templates, but it doesn't make sense for users. VersionID uuid.UUID `json:"template_version_id" validate:"required"` - ParameterValues []CreateParameterRequest `json:"parameter_values"` + ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"` } // CreateWorkspaceRequest provides options for creating a new workspace. @@ -54,7 +54,7 @@ type CreateWorkspaceRequest struct { Name string `json:"name" validate:"username,required"` // ParameterValues allows for additional parameters to be provided // during the initial provision. - ParameterValues []CreateParameterRequest `json:"parameter_values"` + ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"` } func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) { diff --git a/codersdk/users.go b/codersdk/users.go index 693277608d5b8..e44b4ad095f4e 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -21,9 +21,9 @@ const ( ) type UsersRequest struct { - Search string `json:"search"` + Search string `json:"search,omitempty"` // Filter users by status - Status string `json:"status"` + Status string `json:"status,omitempty"` Pagination } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 9d63755ad16de..2fa5dd4b7611a 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -31,9 +31,9 @@ type Workspace struct { // CreateWorkspaceBuildRequest provides options to update the latest workspace build. type CreateWorkspaceBuildRequest struct { - TemplateVersionID uuid.UUID `json:"template_version_id"` + TemplateVersionID uuid.UUID `json:"template_version_id,omitempty"` Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"` - DryRun bool `json:"dry_run"` + DryRun bool `json:"dry_run,omitempty"` ProvisionerState []byte `json:"state,omitempty"` } diff --git a/scripts/apitypings/README.md b/scripts/apitypings/README.md index 32451ec77e5df..079bf8c2c6bbd 100644 --- a/scripts/apitypings/README.md +++ b/scripts/apitypings/README.md @@ -5,7 +5,7 @@ This main.go generates typescript types from the codersdk types in Go. # Features - Supports Go types - - [x] Basics (string/int/etc) + - [x] Basics (string/int/etc) - [x] Maps - [x] Slices - [x] Enums @@ -36,5 +36,4 @@ type InternalType struct { # Future Ideas -- Should `omitempty` in the `json` tag indicate optional? - Use a yaml config for overriding certain types diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts index 60ea7fa8067c0..3936d90b66005 100644 --- a/site/src/api/api.test.ts +++ b/site/src/api/api.test.ts @@ -1,6 +1,6 @@ import axios from "axios" import { getApiKey, login, logout } from "./api" -import { APIKeyResponse, LoginResponse } from "./types" +import * as TypesGen from "./typesGenerated" // Mock the axios module so that no real network requests are made, but rather // we swap in a resolved or rejected value @@ -12,7 +12,7 @@ describe("api.ts", () => { describe("login", () => { it("should return LoginResponse", async () => { // given - const loginResponse: LoginResponse = { + const loginResponse: TypesGen.LoginWithPasswordResponse = { session_token: "abc_123_test", } const axiosMockPost = jest.fn().mockImplementationOnce(() => { @@ -87,7 +87,7 @@ describe("api.ts", () => { describe("getApiKey", () => { it("should return APIKeyResponse", async () => { // given - const apiKeyResponse: APIKeyResponse = { + const apiKeyResponse: TypesGen.GenerateAPIKeyResponse = { key: "abc_123_test", } const axiosMockPost = jest.fn().mockImplementationOnce(() => { diff --git a/site/src/api/api.ts b/site/src/api/api.ts index d300c7aa8a660..0be44bde17fde 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,26 +1,32 @@ import axios, { AxiosRequestHeaders } from "axios" import { mutate } from "swr" -import * as Types from "./types" import * as TypesGen from "./typesGenerated" const CONTENT_TYPE_JSON: AxiosRequestHeaders = { "Content-Type": "application/json", } -export const provisioners: Types.Provisioner[] = [ +export const provisioners: TypesGen.ProvisionerDaemon[] = [ { id: "terraform", name: "Terraform", + created_at: "", + provisioners: [], }, { id: "cdr-basic", name: "Basic", + created_at: "", + provisioners: [], }, ] export namespace Workspace { - export const create = async (request: Types.CreateWorkspaceRequest): Promise => { - const response = await fetch(`/api/v2/organizations/${request.organization_id}/workspaces`, { + export const create = async ( + organizationId: string, + request: TypesGen.CreateWorkspaceRequest, + ): Promise => { + const response = await fetch(`/api/v2/organizations/${organizationId}/workspaces`, { method: "POST", headers: { "Content-Type": "application/json", @@ -43,13 +49,13 @@ export namespace Workspace { } } -export const login = async (email: string, password: string): Promise => { +export const login = async (email: string, password: string): Promise => { const payload = JSON.stringify({ email, password, }) - const response = await axios.post("/api/v2/users/login", payload, { + const response = await axios.post("/api/v2/users/login", payload, { headers: { ...CONTENT_TYPE_JSON }, }) @@ -60,8 +66,8 @@ export const logout = async (): Promise => { await axios.post("/api/v2/users/logout") } -export const getUser = async (): Promise => { - const response = await axios.get("/api/v2/users/me") +export const getUser = async (): Promise => { + const response = await axios.get("/api/v2/users/me") return response.data } @@ -70,8 +76,8 @@ export const getAuthMethods = async (): Promise => { return response.data } -export const getApiKey = async (): Promise => { - const response = await axios.post("/api/v2/users/me/keys") +export const getApiKey = async (): Promise => { + const response = await axios.post("/api/v2/users/me/keys") return response.data } @@ -80,23 +86,23 @@ export const getUsers = async (): Promise => { return response.data } -export const getOrganization = async (organizationId: string): Promise => { - const response = await axios.get(`/api/v2/organizations/${organizationId}`) +export const getOrganization = async (organizationId: string): Promise => { + const response = await axios.get(`/api/v2/organizations/${organizationId}`) return response.data } -export const getOrganizations = async (): Promise => { - const response = await axios.get("/api/v2/users/me/organizations") +export const getOrganizations = async (): Promise => { + const response = await axios.get("/api/v2/users/me/organizations") return response.data } -export const getTemplate = async (templateId: string): Promise => { - const response = await axios.get(`/api/v2/templates/${templateId}`) +export const getTemplate = async (templateId: string): Promise => { + const response = await axios.get(`/api/v2/templates/${templateId}`) return response.data } -export const getWorkspace = async (workspaceId: string): Promise => { - const response = await axios.get(`/api/v2/workspaces/${workspaceId}`) +export const getWorkspace = async (workspaceId: string): Promise => { + const response = await axios.get(`/api/v2/workspaces/${workspaceId}`) return response.data } @@ -104,31 +110,33 @@ export const getWorkspaceByOwnerAndName = async ( organizationID: string, username = "me", workspaceName: string, -): Promise => { - const response = await axios.get( +): Promise => { + const response = await axios.get( `/api/v2/organizations/${organizationID}/workspaces/${username}/${workspaceName}`, ) return response.data } -export const getWorkspaceResources = async (workspaceBuildID: string): Promise => { - const response = await axios.get(`/api/v2/workspacebuilds/${workspaceBuildID}/resources`) +export const getWorkspaceResources = async (workspaceBuildID: string): Promise => { + const response = await axios.get( + `/api/v2/workspacebuilds/${workspaceBuildID}/resources`, + ) return response.data } -export const createUser = async (user: Types.CreateUserRequest): Promise => { +export const createUser = async (user: TypesGen.CreateUserRequest): Promise => { const response = await axios.post("/api/v2/users", user) return response.data } -export const getBuildInfo = async (): Promise => { +export const getBuildInfo = async (): Promise => { const response = await axios.get("/api/v2/buildinfo") return response.data } export const putWorkspaceAutostart = async ( workspaceID: string, - autostart: Types.WorkspaceAutostartRequest, + autostart: TypesGen.UpdateWorkspaceAutostartRequest, ): Promise => { const payload = JSON.stringify(autostart) await axios.put(`/api/v2/workspaces/${workspaceID}/autostart`, payload, { @@ -138,7 +146,7 @@ export const putWorkspaceAutostart = async ( export const putWorkspaceAutostop = async ( workspaceID: string, - autostop: Types.WorkspaceAutostopRequest, + autostop: TypesGen.UpdateWorkspaceAutostopRequest, ): Promise => { const payload = JSON.stringify(autostop) await axios.put(`/api/v2/workspaces/${workspaceID}/autostop`, payload, { @@ -146,7 +154,10 @@ export const putWorkspaceAutostop = async ( }) } -export const updateProfile = async (userId: string, data: Types.UpdateProfileRequest): Promise => { +export const updateProfile = async ( + userId: string, + data: TypesGen.UpdateUserProfileRequest, +): Promise => { const response = await axios.put(`/api/v2/users/${userId}/profile`, data) return response.data } diff --git a/site/src/api/types.ts b/site/src/api/types.ts index bf20e794b8afe..637110cf37275 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -1,101 +1,3 @@ -/** - * `BuildInfoResponse` must be kept in sync with the go struct in buildinfo.go. - */ -export interface BuildInfoResponse { - external_url: string - version: string -} - -export interface LoginResponse { - session_token: string -} - -export interface CreateUserRequest { - username: string - email: string - password: string - organization_id: string -} - -export interface UserResponse { - readonly id: string - readonly username: string - readonly email: string - readonly created_at: string - readonly status: "active" | "suspended" - readonly organization_ids: string[] - readonly roles: { name: string; display_name: string }[] -} - -/** - * `Organization` must be kept in sync with the go struct in organizations.go - */ -export interface Organization { - id: string - name: string - created_at: string - updated_at: string -} - -export interface Provisioner { - id: string - name: string -} - -// This must be kept in sync with the `Template` struct in the back-end -export interface Template { - id: string - created_at: string - updated_at: string - organization_id: string - name: string - provisioner: string - active_version_id: string -} - -export interface CreateTemplateRequest { - name: string - organizationId: string - provisioner: string -} - -export interface CreateWorkspaceRequest { - name: string - template_id: string - organization_id: string -} - -export interface WorkspaceBuild { - id: string -} - -export interface Workspace { - id: string - created_at: string - updated_at: string - owner_id: string - template_id: string - name: string - autostart_schedule: string - autostop_schedule: string - latest_build: WorkspaceBuild -} - -export interface WorkspaceResource { - id: string - agents?: WorkspaceAgent[] -} - -export interface WorkspaceAgent { - id: string - name: string - operating_system: string -} - -export interface APIKeyResponse { - key: string -} - export interface UserAgent { readonly browser: string readonly device: string @@ -103,19 +5,6 @@ export interface UserAgent { readonly os: string } -export interface WorkspaceAutostartRequest { - schedule: string -} - -export interface WorkspaceAutostopRequest { - schedule: string -} - -export interface UpdateProfileRequest { - readonly username: string - readonly email: string -} - export interface ReconnectingPTYRequest { readonly data?: string readonly height?: number diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e30c94e8663e6..d54a8bc88d4a1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -63,18 +63,18 @@ export interface CreateParameterRequest { export interface CreateTemplateRequest { readonly name: string readonly template_version_id: string - readonly parameter_values: CreateParameterRequest[] + readonly parameter_values?: CreateParameterRequest[] } // From codersdk/organizations.go:25:6 export interface CreateTemplateVersionRequest { - readonly template_id: string + readonly template_id?: string // This is likely an enum in an external package ("github.com/coder/coder/coderd/database.ProvisionerStorageMethod") readonly storage_method: string readonly storage_source: string // This is likely an enum in an external package ("github.com/coder/coder/coderd/database.ProvisionerType") readonly provisioner: string - readonly parameter_values: CreateParameterRequest[] + readonly parameter_values?: CreateParameterRequest[] } // From codersdk/users.go:54:6 @@ -87,10 +87,10 @@ export interface CreateUserRequest { // From codersdk/workspaces.go:33:6 export interface CreateWorkspaceBuildRequest { - readonly template_version_id: string + readonly template_version_id?: string // This is likely an enum in an external package ("github.com/coder/coder/coderd/database.WorkspaceTransition") readonly transition: string - readonly dry_run: boolean + readonly dry_run?: boolean readonly state?: string } @@ -98,7 +98,7 @@ export interface CreateWorkspaceBuildRequest { export interface CreateWorkspaceRequest { readonly template_id: string readonly name: string - readonly parameter_values: CreateParameterRequest[] + readonly parameter_values?: CreateParameterRequest[] } // From codersdk/users.go:91:6 @@ -323,8 +323,8 @@ export interface UserRoles { // From codersdk/users.go:23:6 export interface UsersRequest extends Pagination { - readonly search: string - readonly status: string + readonly search?: string + readonly status?: string } // From codersdk/workspaces.go:18:6 diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index f95e7fd0c8757..cc7e8fcdeab76 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -3,7 +3,7 @@ import TextField from "@material-ui/core/TextField" import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" -import { CreateUserRequest } from "../../api/types" +import * as TypesGen from "../../api/typesGenerated" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { FormFooter } from "../FormFooter/FormFooter" import { FullPageForm } from "../FullPageForm/FullPageForm" @@ -21,9 +21,9 @@ export const Language = { } export interface CreateUserFormProps { - onSubmit: (user: CreateUserRequest) => void + onSubmit: (user: TypesGen.CreateUserRequest) => void onCancel: () => void - formErrors?: FormikErrors + formErrors?: FormikErrors isLoading: boolean error?: string myOrgId: string @@ -43,7 +43,7 @@ export const CreateUserForm: React.FC = ({ error, myOrgId, }) => { - const form: FormikContextType = useFormik({ + const form: FormikContextType = useFormik({ initialValues: { email: "", password: "", @@ -53,7 +53,7 @@ export const CreateUserForm: React.FC = ({ validationSchema, onSubmit, }) - const getFieldHelpers = getFormHelpers(form, formErrors) + const getFieldHelpers = getFormHelpers(form, formErrors) return ( diff --git a/site/src/components/Footer/Footer.tsx b/site/src/components/Footer/Footer.tsx index 6dded833c5c5c..c27b5c344aa25 100644 --- a/site/src/components/Footer/Footer.tsx +++ b/site/src/components/Footer/Footer.tsx @@ -3,11 +3,11 @@ import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import { useActor } from "@xstate/react" import React, { useContext } from "react" -import { BuildInfoResponse } from "../../api/types" +import * as TypesGen from "../../api/typesGenerated" import { XServiceContext } from "../../xServices/StateContext" export const Language = { - buildInfoText: (buildInfo: BuildInfoResponse): string => { + buildInfoText: (buildInfo: TypesGen.BuildInfoResponse): string => { return `Coder ${buildInfo.version}` }, } diff --git a/site/src/components/NavbarView/NavbarView.tsx b/site/src/components/NavbarView/NavbarView.tsx index ad2108abf6f47..5f098718cd1e8 100644 --- a/site/src/components/NavbarView/NavbarView.tsx +++ b/site/src/components/NavbarView/NavbarView.tsx @@ -3,14 +3,14 @@ import ListItem from "@material-ui/core/ListItem" import { fade, makeStyles } from "@material-ui/core/styles" import React from "react" import { NavLink } from "react-router-dom" -import { UserResponse } from "../../api/types" +import * as TypesGen from "../../api/typesGenerated" import { navHeight } from "../../theme/constants" import { AdminDropdown } from "../AdminDropdown/AdminDropdown" import { Logo } from "../Icons/Logo" import { UserDropdown } from "../UserDropdown/UsersDropdown" export interface NavbarViewProps { - user?: UserResponse + user?: TypesGen.User onSignOut: () => void } diff --git a/site/src/components/Table/Table.tsx b/site/src/components/Table/Table.tsx index e8445464c9378..82c68a9f9cc93 100644 --- a/site/src/components/Table/Table.tsx +++ b/site/src/components/Table/Table.tsx @@ -8,20 +8,22 @@ import React from "react" import { TableHeaders } from "../TableHeaders/TableHeaders" import { TableTitle } from "../TableTitle/TableTitle" -export interface Column { - /** - * The field of type T that this column is associated with - */ - key: keyof T - /** - * Friendly name of the field, shown in headers - */ - name: string - /** - * Custom render for the field inside the table - */ - renderer?: (field: T[keyof T], data: T) => React.ReactElement -} +export type Column = { + [K in keyof T]: { + /** + * The field of type T that this column is associated with + */ + key: K + /** + * Friendly name of the field, shown in headers + */ + name: string + /** + * Custom render for the field inside the table + */ + renderer?: (field: T[K], data: T) => React.ReactElement + } +}[keyof T] export interface TableProps { /** diff --git a/site/src/components/UserDropdown/UsersDropdown.tsx b/site/src/components/UserDropdown/UsersDropdown.tsx index 503f9517125b2..fad788b95c03f 100644 --- a/site/src/components/UserDropdown/UsersDropdown.tsx +++ b/site/src/components/UserDropdown/UsersDropdown.tsx @@ -7,7 +7,7 @@ import { fade, makeStyles } from "@material-ui/core/styles" import AccountIcon from "@material-ui/icons/AccountCircleOutlined" import React, { useState } from "react" import { Link } from "react-router-dom" -import { UserResponse } from "../../api/types" +import * as TypesGen from "../../api/typesGenerated" import { BorderedMenu } from "../BorderedMenu/BorderedMenu" import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows" import { DocsIcon } from "../Icons/DocsIcon" @@ -21,7 +21,7 @@ export const Language = { signOutLabel: "Sign Out", } export interface UserDropdownProps { - user: UserResponse + user: TypesGen.User onSignOut: () => void } diff --git a/site/src/components/UserProfileCard/UserProfileCard.tsx b/site/src/components/UserProfileCard/UserProfileCard.tsx index 7e542335c5093..dc63b08ea2ef0 100644 --- a/site/src/components/UserProfileCard/UserProfileCard.tsx +++ b/site/src/components/UserProfileCard/UserProfileCard.tsx @@ -1,11 +1,11 @@ import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import React from "react" -import { UserResponse } from "../../api/types" +import * as TypesGen from "../../api/typesGenerated" import { UserAvatar } from "../UserAvatar/UserAvatar" interface UserProfileCardProps { - user: UserResponse + user: TypesGen.User } export const UserProfileCard: React.FC = ({ user }) => { diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index 913ec85ea7c08..ec548cc47d6e8 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -5,7 +5,6 @@ import TableCell from "@material-ui/core/TableCell" import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" import React from "react" -import { UserResponse } from "../../api/types" import * as TypesGen from "../../api/typesGenerated" import { EmptyState } from "../EmptyState/EmptyState" import { RoleSelect } from "../RoleSelect/RoleSelect" @@ -25,10 +24,10 @@ export const Language = { } export interface UsersTableProps { - users: UserResponse[] - onSuspendUser: (user: UserResponse) => void - onResetUserPassword: (user: UserResponse) => void - onUpdateUserRoles: (user: UserResponse, roles: TypesGen.Role["name"][]) => void + users: TypesGen.User[] + onSuspendUser: (user: TypesGen.User) => void + onResetUserPassword: (user: TypesGen.User) => void + onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void roles: TypesGen.Role[] isUpdatingUserRoles?: boolean } diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index b349569b51b29..3e8efdd8d271f 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -5,15 +5,15 @@ import Typography from "@material-ui/core/Typography" import CloudCircleIcon from "@material-ui/icons/CloudCircle" import React from "react" import { Link } from "react-router-dom" -import * as Types from "../../api/types" +import * as TypesGen from "../../api/typesGenerated" import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule" import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection" import * as Constants from "./constants" export interface WorkspaceProps { - organization: Types.Organization - workspace: Types.Workspace - template: Types.Template + organization: TypesGen.Organization + workspace: TypesGen.Workspace + template: TypesGen.Template } /** diff --git a/site/src/forms/CreateTemplateForm.tsx b/site/src/forms/CreateTemplateForm.tsx index 34b5568ccdb38..5242ad1a0c533 100644 --- a/site/src/forms/CreateTemplateForm.tsx +++ b/site/src/forms/CreateTemplateForm.tsx @@ -3,7 +3,7 @@ import { makeStyles } from "@material-ui/core/styles" import { FormikContextType, useFormik } from "formik" import React from "react" import * as Yup from "yup" -import { CreateTemplateRequest, Organization, Provisioner, Template } from "../api/types" +import * as TypesGen from "../api/typesGenerated" import { FormCloseButton } from "../components/FormCloseButton/FormCloseButton" import { FormDropdownField, FormDropdownItem } from "../components/FormDropdownField/FormDropdownField" import { FormSection } from "../components/FormSection/FormSection" @@ -12,13 +12,21 @@ import { FormTitle } from "../components/FormTitle/FormTitle" import { LoadingButton } from "../components/LoadingButton/LoadingButton" import { maxWidth } from "../theme/constants" +// It appears that to create a template you need to create a template version +// and then a template so this contains the information to do both. +export type CreateTemplateRequest = TypesGen.CreateTemplateVersionRequest & Pick + export interface CreateTemplateFormProps { - provisioners: Provisioner[] - organizations: Organization[] - onSubmit: (request: CreateTemplateRequest) => Promise