chore: switch to generated types (#1394)

* Make column renderer use the same type as its key

That way the renderer only takes `string` for example when rendering the
name field instead of `string | number` when the interface has some
fields that are strings and some fields are numbers.

This will be necessary when switching to generated types since some of
the fields are numbers (like the owner count on a template).

* Switch fully to generated types

In some places the organization ID is part of the URL but not part of
the request so I separated out the ID into a separate argument in the
relevant API functions.

Otherwise this was a straightforward replacement where I mostly only
needed to change some of the interface names (User instead of
UserResponse for example) and add a few missing but required properties.

I kind of winged the template form; I am not sure what the difference
between a template and template version is or why the latter comes
before the former so the form just returns all the data required to
create both.

* Delete handwritten types

Except for UserAgent which seems to be purely frontend and
ReconnectingPTYRequest which is not in codersdk so I am just leaving it
for now.

* Remove implemented omitempty as a future idea

This was implemented in 2d3dc436a8.

* Add missing optionalities to generated request interfaces
This commit is contained in:
Asher 2022-05-12 10:01:28 -05:00 committed by GitHub
parent 56076a0aa2
commit 26b04cc96f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 232 additions and 295 deletions

View File

@ -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) {

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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

View File

@ -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(() => {

View File

@ -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<Types.Workspace> => {
const response = await fetch(`/api/v2/organizations/${request.organization_id}/workspaces`, {
export const create = async (
organizationId: string,
request: TypesGen.CreateWorkspaceRequest,
): Promise<TypesGen.Workspace> => {
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<Types.LoginResponse> => {
export const login = async (email: string, password: string): Promise<TypesGen.LoginWithPasswordResponse> => {
const payload = JSON.stringify({
email,
password,
})
const response = await axios.post<Types.LoginResponse>("/api/v2/users/login", payload, {
const response = await axios.post<TypesGen.LoginWithPasswordResponse>("/api/v2/users/login", payload, {
headers: { ...CONTENT_TYPE_JSON },
})
@ -60,8 +66,8 @@ export const logout = async (): Promise<void> => {
await axios.post("/api/v2/users/logout")
}
export const getUser = async (): Promise<Types.UserResponse> => {
const response = await axios.get<Types.UserResponse>("/api/v2/users/me")
export const getUser = async (): Promise<TypesGen.User> => {
const response = await axios.get<TypesGen.User>("/api/v2/users/me")
return response.data
}
@ -70,8 +76,8 @@ export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
return response.data
}
export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
const response = await axios.post<Types.APIKeyResponse>("/api/v2/users/me/keys")
export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
const response = await axios.post<TypesGen.GenerateAPIKeyResponse>("/api/v2/users/me/keys")
return response.data
}
@ -80,23 +86,23 @@ export const getUsers = async (): Promise<TypesGen.User[]> => {
return response.data
}
export const getOrganization = async (organizationId: string): Promise<Types.Organization> => {
const response = await axios.get<Types.Organization>(`/api/v2/organizations/${organizationId}`)
export const getOrganization = async (organizationId: string): Promise<TypesGen.Organization> => {
const response = await axios.get<TypesGen.Organization>(`/api/v2/organizations/${organizationId}`)
return response.data
}
export const getOrganizations = async (): Promise<Types.Organization[]> => {
const response = await axios.get<Types.Organization[]>("/api/v2/users/me/organizations")
export const getOrganizations = async (): Promise<TypesGen.Organization[]> => {
const response = await axios.get<TypesGen.Organization[]>("/api/v2/users/me/organizations")
return response.data
}
export const getTemplate = async (templateId: string): Promise<Types.Template> => {
const response = await axios.get<Types.Template>(`/api/v2/templates/${templateId}`)
export const getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
const response = await axios.get<TypesGen.Template>(`/api/v2/templates/${templateId}`)
return response.data
}
export const getWorkspace = async (workspaceId: string): Promise<Types.Workspace> => {
const response = await axios.get<Types.Workspace>(`/api/v2/workspaces/${workspaceId}`)
export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
return response.data
}
@ -104,31 +110,33 @@ export const getWorkspaceByOwnerAndName = async (
organizationID: string,
username = "me",
workspaceName: string,
): Promise<Types.Workspace> => {
const response = await axios.get<Types.Workspace>(
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(
`/api/v2/organizations/${organizationID}/workspaces/${username}/${workspaceName}`,
)
return response.data
}
export const getWorkspaceResources = async (workspaceBuildID: string): Promise<Types.WorkspaceResource[]> => {
const response = await axios.get<Types.WorkspaceResource[]>(`/api/v2/workspacebuilds/${workspaceBuildID}/resources`)
export const getWorkspaceResources = async (workspaceBuildID: string): Promise<TypesGen.WorkspaceResource[]> => {
const response = await axios.get<TypesGen.WorkspaceResource[]>(
`/api/v2/workspacebuilds/${workspaceBuildID}/resources`,
)
return response.data
}
export const createUser = async (user: Types.CreateUserRequest): Promise<TypesGen.User> => {
export const createUser = async (user: TypesGen.CreateUserRequest): Promise<TypesGen.User> => {
const response = await axios.post<TypesGen.User>("/api/v2/users", user)
return response.data
}
export const getBuildInfo = async (): Promise<Types.BuildInfoResponse> => {
export const getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
const response = await axios.get("/api/v2/buildinfo")
return response.data
}
export const putWorkspaceAutostart = async (
workspaceID: string,
autostart: Types.WorkspaceAutostartRequest,
autostart: TypesGen.UpdateWorkspaceAutostartRequest,
): Promise<void> => {
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<void> => {
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<Types.UserResponse> => {
export const updateProfile = async (
userId: string,
data: TypesGen.UpdateUserProfileRequest,
): Promise<TypesGen.User> => {
const response = await axios.put(`/api/v2/users/${userId}/profile`, data)
return response.data
}

View File

@ -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

View File

@ -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

View File

@ -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<CreateUserRequest>
formErrors?: FormikErrors<TypesGen.CreateUserRequest>
isLoading: boolean
error?: string
myOrgId: string
@ -43,7 +43,7 @@ export const CreateUserForm: React.FC<CreateUserFormProps> = ({
error,
myOrgId,
}) => {
const form: FormikContextType<CreateUserRequest> = useFormik<CreateUserRequest>({
const form: FormikContextType<TypesGen.CreateUserRequest> = useFormik<TypesGen.CreateUserRequest>({
initialValues: {
email: "",
password: "",
@ -53,7 +53,7 @@ export const CreateUserForm: React.FC<CreateUserFormProps> = ({
validationSchema,
onSubmit,
})
const getFieldHelpers = getFormHelpers<CreateUserRequest>(form, formErrors)
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(form, formErrors)
return (
<FullPageForm title="Create user" onCancel={onCancel}>

View File

@ -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}`
},
}

View File

@ -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
}

View File

@ -8,20 +8,22 @@ import React from "react"
import { TableHeaders } from "../TableHeaders/TableHeaders"
import { TableTitle } from "../TableTitle/TableTitle"
export interface Column<T> {
/**
* 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<T> = {
[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<T> {
/**

View File

@ -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
}

View File

@ -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<UserProfileCardProps> = ({ user }) => {

View File

@ -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
}

View File

@ -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
}
/**

View File

@ -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<TypesGen.CreateTemplateRequest, "name">
export interface CreateTemplateFormProps {
provisioners: Provisioner[]
organizations: Organization[]
onSubmit: (request: CreateTemplateRequest) => Promise<Template>
provisioners: TypesGen.ProvisionerDaemon[]
organizations: TypesGen.Organization[]
onSubmit: (organizationId: string, request: CreateTemplateRequest) => Promise<TypesGen.Template>
onCancel: () => void
}
interface CreateTemplateFields extends Pick<CreateTemplateRequest, "name" | "provisioner"> {
organizationId: string
}
const validationSchema = Yup.object({
provisioner: Yup.string().required("Provisioner is required."),
organizationId: Yup.string().required("Organization is required."),
@ -33,7 +41,7 @@ export const CreateTemplateForm: React.FC<CreateTemplateFormProps> = ({
}) => {
const styles = useStyles()
const form: FormikContextType<CreateTemplateRequest> = useFormik<CreateTemplateRequest>({
const form: FormikContextType<CreateTemplateFields> = useFormik<CreateTemplateFields>({
initialValues: {
provisioner: provisioners[0].id,
organizationId: organizations[0].name,
@ -42,7 +50,12 @@ export const CreateTemplateForm: React.FC<CreateTemplateFormProps> = ({
enableReinitialize: true,
validationSchema: validationSchema,
onSubmit: (req) => {
return onSubmit(req)
return onSubmit(req.organizationId, {
name: req.name,
storage_method: "file",
storage_source: "hash",
provisioner: req.provisioner,
})
},
})

View File

@ -15,7 +15,7 @@ describe("CreateWorkspaceForm", () => {
template={MockTemplate}
onSubmit={onSubmit}
onCancel={onCancel}
organization_id={MockOrganization.id}
organizationId={MockOrganization.id}
/>,
)

View File

@ -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 { CreateWorkspaceRequest, Template, Workspace } from "../api/types"
import * as TypesGen from "../api/typesGenerated"
import { FormCloseButton } from "../components/FormCloseButton/FormCloseButton"
import { FormSection } from "../components/FormSection/FormSection"
import { FormTextField } from "../components/FormTextField/FormTextField"
@ -12,10 +12,10 @@ import { LoadingButton } from "../components/LoadingButton/LoadingButton"
import { maxWidth } from "../theme/constants"
export interface CreateWorkspaceForm {
template: Template
onSubmit: (request: CreateWorkspaceRequest) => Promise<Workspace>
template: TypesGen.Template
onSubmit: (organizationId: string, request: TypesGen.CreateWorkspaceRequest) => Promise<TypesGen.Workspace>
onCancel: () => void
organization_id: string
organizationId: string
}
const validationSchema = Yup.object({
@ -26,7 +26,7 @@ export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({
template,
onSubmit,
onCancel,
organization_id,
organizationId,
}) => {
const styles = useStyles()
@ -37,10 +37,9 @@ export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({
enableReinitialize: true,
validationSchema: validationSchema,
onSubmit: ({ name }) => {
return onSubmit({
return onSubmit(organizationId, {
template_id: template.id,
name: name,
organization_id,
})
},
})

View File

@ -4,7 +4,7 @@ import React, { useCallback, useContext } from "react"
import { useNavigate, useParams } from "react-router-dom"
import useSWR from "swr"
import * as API from "../../../../api/api"
import * as Types from "../../../../api/types"
import * as TypesGen from "../../../../api/typesGenerated"
import { ErrorSummary } from "../../../../components/ErrorSummary/ErrorSummary"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm"
@ -20,11 +20,11 @@ export const CreateWorkspacePage: React.FC = () => {
const xServices = useContext(XServiceContext)
const myOrgId = useSelector(xServices.authXService, selectOrgId)
const { data: organizationInfo, error: organizationError } = useSWR<Types.Organization, Error>(
const { data: organizationInfo, error: organizationError } = useSWR<TypesGen.Organization, Error>(
() => `/api/v2/users/me/organizations/${organizationName}`,
)
const { data: template, error: templateError } = useSWR<Types.Template, Error>(() => {
const { data: template, error: templateError } = useSWR<TypesGen.Template, Error>(() => {
return `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/templates/${templateName}`
})
@ -32,8 +32,8 @@ export const CreateWorkspacePage: React.FC = () => {
navigate(`/templates/${organizationName}/${templateName}`)
}, [navigate, organizationName, templateName])
const onSubmit = async (req: Types.CreateWorkspaceRequest) => {
const workspace = await API.Workspace.create(req)
const onSubmit = async (organizationId: string, req: TypesGen.CreateWorkspaceRequest) => {
const workspace = await API.Workspace.create(organizationId, req)
navigate(`/workspaces/${workspace.id}`)
return workspace
}
@ -56,7 +56,7 @@ export const CreateWorkspacePage: React.FC = () => {
return (
<div className={styles.root}>
<CreateWorkspaceForm onCancel={onCancel} onSubmit={onSubmit} template={template} organization_id={myOrgId} />
<CreateWorkspaceForm onCancel={onCancel} onSubmit={onSubmit} template={template} organizationId={myOrgId} />
</div>
)
}

View File

@ -1,7 +1,7 @@
import React from "react"
import { Link, useNavigate, useParams } from "react-router-dom"
import useSWR from "swr"
import { Organization, Template, Workspace, WorkspaceBuild } from "../../../../api/types"
import * as TypesGen from "../../../../api/typesGenerated"
import { EmptyState } from "../../../../components/EmptyState/EmptyState"
import { ErrorSummary } from "../../../../components/ErrorSummary/ErrorSummary"
import { Header } from "../../../../components/Header/Header"
@ -16,18 +16,18 @@ export const TemplatePage: React.FC = () => {
const navigate = useNavigate()
const { template: templateName, organization: organizationName } = useParams()
const { data: organizationInfo, error: organizationError } = useSWR<Organization, Error>(
const { data: organizationInfo, error: organizationError } = useSWR<TypesGen.Organization, Error>(
() => `/api/v2/users/me/organizations/${organizationName}`,
)
const { data: templateInfo, error: templateError } = useSWR<Template, Error>(
const { data: templateInfo, error: templateError } = useSWR<TypesGen.Template, Error>(
() => `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/templates/${templateName}`,
)
// This just grabs all workspaces... and then later filters them to match the
// current template.
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(
const { data: workspaces, error: workspacesError } = useSWR<TypesGen.Workspace[], Error>(
() => `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/workspaces`,
)
@ -62,11 +62,11 @@ export const TemplatePage: React.FC = () => {
/>
)
const columns: Column<Workspace>[] = [
const columns: Column<TypesGen.Workspace>[] = [
{
key: "name",
name: "Name",
renderer: (nameField: string | WorkspaceBuild, workspace: Workspace) => {
renderer: (nameField: string | TypesGen.WorkspaceBuild, workspace: TypesGen.Workspace) => {
return <Link to={`/workspaces/${workspace.id}`}>{nameField}</Link>
},
},

View File

@ -2,7 +2,7 @@ import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { Link } from "react-router-dom"
import useSWR from "swr"
import { Organization, Template } from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
import { CodeExample } from "../../components/CodeExample/CodeExample"
import { EmptyState } from "../../components/EmptyState/EmptyState"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
@ -14,8 +14,8 @@ import { Column, Table } from "../../components/Table/Table"
export const TemplatesPage: React.FC = () => {
const styles = useStyles()
const { data: orgs, error: orgsError } = useSWR<Organization[], Error>("/api/v2/users/me/organizations")
const { data: templates, error } = useSWR<Template[] | null, Error>(
const { data: orgs, error: orgsError } = useSWR<TypesGen.Organization[], Error>("/api/v2/users/me/organizations")
const { data: templates, error } = useSWR<TypesGen.Template[] | null, Error>(
orgs ? `/api/v2/organizations/${orgs[0].id}/templates` : null,
)
@ -33,18 +33,18 @@ export const TemplatesPage: React.FC = () => {
// Create a dictionary of organization ID -> organization Name
// Needed to properly construct links to dive into a template
const orgDictionary = orgs.reduce((acc: Record<string, string>, curr: Organization) => {
const orgDictionary = orgs.reduce((acc: Record<string, string>, curr: TypesGen.Organization) => {
return {
...acc,
[curr.id]: curr.name,
}
}, {})
const columns: Column<Template>[] = [
const columns: Column<TypesGen.Template>[] = [
{
key: "name",
name: "Name",
renderer: (nameField: string, data: Template) => {
renderer: (nameField: string, data: TypesGen.Template) => {
return <Link to={`/templates/${orgDictionary[data.organization_id]}/${nameField}`}>{nameField}</Link>
},
},

View File

@ -1,7 +1,7 @@
import { useActor, useSelector } from "@xstate/react"
import React, { useContext } from "react"
import { useNavigate } from "react-router"
import { CreateUserRequest } from "../../../api/types"
import * as TypesGen from "../../../api/typesGenerated"
import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm"
import { Margins } from "../../../components/Margins/Margins"
import { selectOrgId } from "../../../xServices/auth/authSelectors"
@ -25,7 +25,7 @@ export const CreateUserPage: React.FC = () => {
<Margins>
<CreateUserForm
formErrors={createUserFormErrors}
onSubmit={(user: CreateUserRequest) => usersSend({ type: "CREATE", user })}
onSubmit={(user: TypesGen.CreateUserRequest) => usersSend({ type: "CREATE", user })}
onCancel={() => navigate("/users")}
isLoading={usersState.hasTag("loading")}
error={genericError}

View File

@ -1,5 +1,4 @@
import React from "react"
import { UserResponse } from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { Header } from "../../components/Header/Header"
@ -13,11 +12,11 @@ export const Language = {
}
export interface UsersPageViewProps {
users: UserResponse[]
users: TypesGen.User[]
openUserCreationDialog: () => void
onSuspendUser: (user: UserResponse) => void
onResetUserPassword: (user: UserResponse) => void
onUpdateUserRoles: (user: UserResponse, roles: TypesGen.Role["name"][]) => void
onSuspendUser: (user: TypesGen.User) => void
onResetUserPassword: (user: TypesGen.User) => void
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
roles: TypesGen.Role[]
error?: unknown
isUpdatingUserRoles?: boolean

View File

@ -1,44 +1,37 @@
import {
BuildInfoResponse,
Organization,
Provisioner,
Template,
UserAgent,
UserResponse,
Workspace,
WorkspaceAgent,
WorkspaceAutostartRequest,
WorkspaceResource,
} from "../api/types"
import { AuthMethods, Role } from "../api/typesGenerated"
import * as Types from "../api/types"
import * as TypesGen from "../api/typesGenerated"
export const MockSessionToken = { session_token: "my-session-token" }
export const MockSessionToken: TypesGen.LoginWithPasswordResponse = {
session_token: "my-session-token",
}
export const MockAPIKey = { key: "my-api-key" }
export const MockAPIKey: TypesGen.GenerateAPIKeyResponse = {
key: "my-api-key",
}
export const MockBuildInfo: BuildInfoResponse = {
export const MockBuildInfo: TypesGen.BuildInfoResponse = {
external_url: "file:///mock-url",
version: "v99.999.9999+c9cdf14",
}
export const MockAdminRole: Role = {
export const MockAdminRole: TypesGen.Role = {
name: "admin",
display_name: "Admin",
}
export const MockMemberRole: Role = {
export const MockMemberRole: TypesGen.Role = {
name: "member",
display_name: "Member",
}
export const MockAuditorRole: Role = {
export const MockAuditorRole: TypesGen.Role = {
name: "auditor",
display_name: "Auditor",
}
export const MockSiteRoles = [MockAdminRole, MockAuditorRole, MockMemberRole]
export const MockUser: UserResponse = {
export const MockUser: TypesGen.User = {
id: "test-user",
username: "TestUser",
email: "test@coder.com",
@ -48,7 +41,7 @@ export const MockUser: UserResponse = {
roles: [MockAdminRole, MockMemberRole],
}
export const MockUser2: UserResponse = {
export const MockUser2: TypesGen.User = {
id: "test-user-2",
username: "TestUser2",
email: "test2@coder.com",
@ -58,19 +51,27 @@ export const MockUser2: UserResponse = {
roles: [MockMemberRole],
}
export const MockOrganization: Organization = {
export const MockOrganization: TypesGen.Organization = {
id: "test-org",
name: "Test Organization",
created_at: "",
updated_at: "",
}
export const MockProvisioner: Provisioner = {
export const MockProvisioner: TypesGen.ProvisionerDaemon = {
created_at: "",
id: "test-provisioner",
name: "Test Provisioner",
provisioners: [],
}
export const MockTemplate: Template = {
export const MockProvisionerJob: TypesGen.ProvisionerJob = {
created_at: "",
id: "test-provisioner-job",
status: "succeeded",
}
export const MockTemplate: TypesGen.Template = {
id: "test-template",
created_at: "",
updated_at: "",
@ -78,60 +79,86 @@ export const MockTemplate: Template = {
name: "Test Template",
provisioner: MockProvisioner.id,
active_version_id: "",
workspace_owner_count: 1,
}
export const MockWorkspaceAutostartDisabled: WorkspaceAutostartRequest = {
export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = {
schedule: "",
}
export const MockWorkspaceAutostartEnabled: WorkspaceAutostartRequest = {
export const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartRequest = {
// Runs at 9:30am Monday through Friday using Canada/Eastern
// (America/Toronto) time
schedule: "CRON_TZ=Canada/Eastern 30 9 * * 1-5",
}
export const MockWorkspaceAutostopDisabled: WorkspaceAutostartRequest = {
export const MockWorkspaceAutostopDisabled: TypesGen.UpdateWorkspaceAutostartRequest = {
schedule: "",
}
export const MockWorkspaceAutostopEnabled: WorkspaceAutostartRequest = {
export const MockWorkspaceAutostopEnabled: TypesGen.UpdateWorkspaceAutostartRequest = {
// Runs at 9:30pm Monday through Friday using America/Toronto
schedule: "CRON_TZ=America/Toronto 30 21 * * 1-5",
}
export const MockWorkspace: Workspace = {
export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = {
after_id: "",
before_id: "",
created_at: "",
id: "test-workspace-build",
initiator_id: "",
job: MockProvisionerJob,
name: "a-workspace-build",
template_version_id: "",
transition: "start",
updated_at: "",
workspace_id: "test-workspace",
}
export const MockWorkspace: TypesGen.Workspace = {
id: "test-workspace",
name: "Test-Workspace",
created_at: "",
updated_at: "",
template_id: MockTemplate.id,
template_name: MockTemplate.name,
outdated: false,
owner_id: MockUser.id,
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
autostop_schedule: MockWorkspaceAutostopEnabled.schedule,
latest_build: {
id: "test-workspace-build",
},
latest_build: MockWorkspaceBuild,
}
export const MockWorkspaceAgent: WorkspaceAgent = {
export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
architecture: "amd64",
created_at: "",
environment_variables: {},
id: "test-workspace-agent",
name: "a-workspace-agent",
operating_system: "linux",
resource_id: "",
status: "connected",
updated_at: "",
}
export const MockWorkspaceResource: WorkspaceResource = {
id: "test-workspace-resource",
export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
agents: [MockWorkspaceAgent],
created_at: "",
id: "test-workspace-resource",
job_id: "",
name: "a-workspace-resource",
type: "google_compute_disk",
workspace_transition: "start",
}
export const MockUserAgent: UserAgent = {
export const MockUserAgent: Types.UserAgent = {
browser: "Chrome 99.0.4844",
device: "Other",
ip_address: "11.22.33.44",
os: "Windows 10",
}
export const MockAuthMethods: AuthMethods = {
export const MockAuthMethods: TypesGen.AuthMethods = {
password: true,
github: false,
}

View File

@ -1,6 +1,5 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import * as Types from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
import { displaySuccess } from "../../components/GlobalSnackbar/utils"
@ -13,14 +12,14 @@ export interface AuthContext {
getMethodsError?: Error | unknown
authError?: Error | unknown
updateProfileError?: Error | unknown
me?: Types.UserResponse
me?: TypesGen.User
methods?: TypesGen.AuthMethods
}
export type AuthEvent =
| { type: "SIGN_OUT" }
| { type: "SIGN_IN"; email: string; password: string }
| { type: "UPDATE_PROFILE"; data: Types.UpdateProfileRequest }
| { type: "UPDATE_PROFILE"; data: TypesGen.UpdateUserProfileRequest }
export const authMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdLAJZQB2kA8hgMTYCSA4gHID6DLioADgPal0JPuW4gAHogDsABgCshOQA4AzABY5ARgBMUgJxrtcgDQgAnok0zNSwkpkrNKvQDY3aqS6kBfb6bRYuPhEpBQk5FAM5LQQIkThAG58ANYhZORRYvyCwqJIEohyMlKE2mrFKo7aLq5SJuaILtq2mjZqrboy2s4+fiABOHgExOnhkdFgAE6TfJOEPAA2+ABmswC2IxSZ+dkkQiJikgiyCsrqWroGRqYWCHoq2oQyDpouXa1qGmq+-hiDwYQYOghBEAKqwKYxOKERIpIhAgCyYCyAj2uUOiF0mieTik3UqMhqSleN0QhgUOhUbzUKhk9jKch+-T+QWGQJBUHBkKmMzmixW60BYHQSJROQO+SOUmxVKUniUcjkUgVLjlpOOCqecj0LyKmmVeiUTIGrPhwo5SKwfAgsChlBh5CSqSFIuFmGt8B2qP2eVARxUeNKCo02k0cllTRc6qUalsCtU731DikvV+gSGZuBY0t7pttB5s3mS3Qq0mG0Rbo9YrREr9iHUMkIUnKHSklQVKnqtz0BkImjUbmeShcVmejL6Jozm0oECi8xmyxIC3iEGXtFBAAUACIAQQAKgBRNgbgBKVAAYgwADIH6s+jEIOV6Qgj1oxnTBkfq7qNlxyT4aC4saaPcVLGiyU6hDOc48AuS5EKgPAQPgYwbnBa6xPasLOpOAJQZAMHoQhSEoREaF8Iuy4ILCADGKEiAA2jIAC6d7opKiBPi+rRtB+-5fg0CDqM+YafG8+jWLI2jgemeHpAR5DzhR8GEIhyEcuRlFgPm0yFvyJaCrhwz4bOimwcpy6qSRGlEdRjp8HRPpMaxXrir6BSPvo3Fvu0zT8Zo6oDmoTb-p8cjaFcsjqDJ-zGfJpn0Mw7BUKCe5sbWHm6CU2jWKGnhaFSeLqv2jZKMOehOE49i0v+MWmtOYw0OgdrxPZzpQU16XuUcAC0-aPGGSgVQOTS4ko2jfjGhC0gB1WeDIBguHVkGjBETU6byRYCmW06da5NbdZiKalLl+p-k4XgTYJNhxuVNKGCB77fEy5DWnAYhGWkFDUBgXUPoqLiKOoxIqGVejNKoxXPCUMgjZ8zj6sORoThBclhBE2y8N67F1o+xIvhoejavqEX-gFgl6IGFWOC2bzWNqy0AuyYxcpMf0cQghjqn+JT6GJeJynIqrI2msWZhalY2uzuN9dojwpn+epDvqHjRkUL7KBDEljiLzKyXF32mUpWkwquRCvQeuls-t94c606s8c0A5C00UjqqDja5cUXQRXiDiMwb0FmURpuWQW1tY25D7242jsxn+bi6IFhh2K4qjKP+fsqAHX1B8bKkkGb0seR0gNx87idu4JtIwzoxTdELzbheOov1SZhEWcR6moURxdHCOgPhsBoNDRDKju84hCfHS4ZAXPqg59OCn58ufeFODQPD2DY-FUqGsASmdShgvKP67nClrwgQu2EPIPb2V4+CVNf7w5TOphs22en2LDVrb9Ns4w8j1coU8iafDKJVWQagDDqmOsOKBVhXDgweNJb+ppL59TcH2ZQw1E5jSurcYK8CHCODfE0B4vRfBAA */
@ -40,16 +39,16 @@ export const authMachine =
events: {} as AuthEvent,
services: {} as {
getMe: {
data: Types.UserResponse
data: TypesGen.User
}
getMethods: {
data: TypesGen.AuthMethods
}
signIn: {
data: Types.LoginResponse
data: TypesGen.LoginWithPasswordResponse
}
updateProfile: {
data: Types.UserResponse
data: TypesGen.User
}
},
},

View File

@ -1,10 +1,10 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import * as Types from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
export interface BuildInfoContext {
getBuildInfoError?: Error | unknown
buildInfo?: Types.BuildInfoResponse
buildInfo?: TypesGen.BuildInfoResponse
}
export const buildInfoMachine = createMachine(
@ -14,7 +14,7 @@ export const buildInfoMachine = createMachine(
context: {} as BuildInfoContext,
services: {} as {
getBuildInfo: {
data: Types.BuildInfoResponse
data: TypesGen.BuildInfoResponse
}
},
},

View File

@ -1,13 +1,14 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import * as Types from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
export interface TerminalContext {
organizationsError?: Error | unknown
organizations?: Types.Organization[]
organizations?: TypesGen.Organization[]
workspaceError?: Error | unknown
workspace?: Types.Workspace
workspaceAgent?: Types.WorkspaceAgent
workspace?: TypesGen.Workspace
workspaceAgent?: TypesGen.WorkspaceAgent
workspaceAgentError?: Error | unknown
websocket?: WebSocket
websocketError?: Error | unknown
@ -34,13 +35,13 @@ export const terminalMachine =
events: {} as TerminalEvent,
services: {} as {
getOrganizations: {
data: Types.Organization[]
data: TypesGen.Organization[]
}
getWorkspace: {
data: Types.Workspace
data: TypesGen.Workspace
}
getWorkspaceAgent: {
data: Types.WorkspaceAgent
data: TypesGen.WorkspaceAgent
}
connect: {
data: WebSocket

View File

@ -1,7 +1,6 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors"
import * as Types from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
import { generateRandomString } from "../../util/random"
@ -36,7 +35,7 @@ export interface UsersContext {
export type UsersEvent =
| { type: "GET_USERS" }
| { type: "CREATE"; user: Types.CreateUserRequest }
| { type: "CREATE"; user: TypesGen.CreateUserRequest }
// Suspend events
| { type: "SUSPEND_USER"; userId: TypesGen.User["id"] }
| { type: "CONFIRM_USER_SUSPENSION" }

View File

@ -1,11 +1,11 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import * as Types from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
interface WorkspaceContext {
workspace?: Types.Workspace
template?: Types.Template
organization?: Types.Organization
workspace?: TypesGen.Workspace
template?: TypesGen.Template
organization?: TypesGen.Organization
getWorkspaceError?: Error | unknown
getTemplateError?: Error | unknown
getOrganizationError?: Error | unknown
@ -21,13 +21,13 @@ export const workspaceMachine = createMachine(
events: {} as WorkspaceEvent,
services: {} as {
getWorkspace: {
data: Types.Workspace
data: TypesGen.Workspace
}
getTemplate: {
data: Types.Template
data: TypesGen.Template
}
getOrganization: {
data: Types.Organization
data: TypesGen.Organization
}
},
},