Skip to content

Try swagger-typescript-api for real #535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a8b0767
working get projects request
david-crespo Nov 27, 2021
79d157c
basically functioning typed hook
david-crespo Nov 27, 2021
9f407ed
simpler error handling, nicer comment
david-crespo Nov 28, 2021
db6236d
be sneaky with imports to show it's a drop-in replacement
david-crespo Nov 28, 2021
d7deee0
make STA a dev dep instead of using npx
david-crespo Nov 29, 2021
f5aba99
show corrected params output with forked STA
david-crespo Nov 29, 2021
057a7db
everything works except the login page
david-crespo Nov 29, 2021
d2b6ca6
delete openapi-generator client
david-crespo Nov 29, 2021
a5a7816
make mutate() argument look more like before
david-crespo Dec 1, 2021
72cec66
Merge main into swagger-typescript-api
david-crespo Dec 6, 2021
111d3aa
fix type errors on everything but login
david-crespo Dec 6, 2021
e2e6b66
bump API version, why not
david-crespo Dec 6, 2021
b99af6b
automatically update packer-id
github-actions[bot] Dec 6, 2021
2761532
fix... everything? (except the tests)
david-crespo Dec 6, 2021
6c49453
attempt to install git on the GCP VM
david-crespo Dec 6, 2021
9863895
automatically update packer-id
github-actions[bot] Dec 6, 2021
b564bbd
tests pass
david-crespo Dec 6, 2021
358bdc9
don't be scared, extract the whole onSuccess
david-crespo Dec 6, 2021
4fe9652
use package-patch instead of swagger (including patched template)
david-crespo Dec 7, 2021
8f4836f
don't need git if we're not using a fork
david-crespo Dec 7, 2021
fbb5e82
automatically update packer-id
github-actions[bot] Dec 7, 2021
a6fb60d
remove codemods, update link in readme
zephraph Dec 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/packer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
CLOUDFLARE_TOKEN: ${{secrets.CLOUDFLARE_TOKEN}}
SSL_CERT: ${{secrets.SSL_CERT}}
SSL_KEY: ${{secrets.SSL_KEY}}
API_VERSION: de84bb85d6ca264b1adddac9822e173aeb2b433a
API_VERSION: 038dbd04fbc292b5f7880c0d0c723ef182145cb2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I've seriously got to fix this. If the file isn't changed in the current PR, this shouldn't be ran. Why it happens anyway is a mystery to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it watches the packer directory, which did have changes


# get the image information from gcloud
- name: Get image information
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Web client to the [Oxide control plane API](https://github.com/oxidecomputer/omi
- [React](https://reactjs.org/) (+ [React Router](https://reactrouter.com/), [React Query](https://react-query.tanstack.com), [React Table](https://react-table.tanstack.com))
- [Jest](https://jestjs.io/) + [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
- [Tailwind](https://tailwindcss.com/)
- [OpenAPI Generator](https://openapi-generator.tech/) (generates typed API client from Nexus's [OpenAPI spec](app/docs/nexus-openapi.json))
- [OpenAPI Generator](https://github.com/acacode/swagger-typescript-api) (generates typed API client from Nexus's [OpenAPI spec](app/docs/nexus-openapi.json))
- [Vite](https://vitejs.dev/)
- [Storybook](https://storybook.js.org/) (see main branch Storybook [here](https://console-ui-storybook.vercel.app/))

Expand Down
10 changes: 5 additions & 5 deletions app/components/InstancesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const columns = [
className="!no-underline"
title={value.timeRunStateUpdated.toLocaleString()}
>
{timeAgoAbbr(value.timeRunStateUpdated)}
{timeAgoAbbr(new Date(value.timeRunStateUpdated))}
</abbr>
</span>
),
Expand Down Expand Up @@ -171,7 +171,7 @@ export const InstancesTable = ({ className }: { className?: string }) => {
{
organizationName: orgName,
projectName,
pageToken: currentPage,
page_token: currentPage,
limit: PAGE_SIZE,
},
{ refetchInterval: 5000, keepPreviousData: true }
Expand Down Expand Up @@ -223,13 +223,13 @@ export const InstancesTable = ({ className }: { className?: string }) => {
</PageButton>
<PageButton
onClick={() =>
instances.nextPage && goToNextPage(instances.nextPage)
instances.next_page && goToNextPage(instances.next_page)
}
disabled={!instances.nextPage}
disabled={!instances.next_page}
aria-label="Next"
>
{/* filled triangle right, outline triangle right */}
{instances.nextPage ? '\u25B6' : '\u25B7'}
{instances.next_page ? '\u25B6' : '\u25B7'}
</PageButton>
</span>
</div>
Expand Down
11 changes: 4 additions & 7 deletions app/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useToast } from '../hooks'
export default function LoginPage() {
const navigate = useNavigate()
const addToast = useToast()
// TODO these are not under organizations. annoying. should really just use plain react-query
const loginPost = useApiMutation('spoofLogin', {
onSuccess: () => {
addToast({
Expand Down Expand Up @@ -58,9 +59,7 @@ export default function LoginPage() {
variant="solid"
className="w-full"
disabled={loginPost.isLoading}
onClick={() =>
loginPost.mutate({ loginParams: { username: 'privileged' } })
}
onClick={() => loginPost.mutate({ body: { username: 'privileged' } })}
>
Privileged
</Button>
Expand All @@ -70,7 +69,7 @@ export default function LoginPage() {
className="w-full"
disabled={loginPost.isLoading}
onClick={() =>
loginPost.mutate({ loginParams: { username: 'unprivileged' } })
loginPost.mutate({ body: { username: 'unprivileged' } })
}
>
Unprivileged
Expand All @@ -80,9 +79,7 @@ export default function LoginPage() {
variant="ghost"
className="w-full"
disabled={loginPost.isLoading}
onClick={() =>
loginPost.mutate({ loginParams: { username: 'other' } })
}
onClick={() => loginPost.mutate({ body: { username: 'other' } })}
>
Bad Request
</Button>
Expand Down
63 changes: 36 additions & 27 deletions app/pages/ProjectCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Success16Icon,
FieldTitle,
} from '@oxide/ui'
import type { Project } from '@oxide/api'
import { useApiMutation, useApiQueryClient } from '@oxide/api'
import { useParams, useToast } from '../hooks'
import { getServerError } from '../util/errors'
Expand All @@ -24,40 +25,23 @@ const ERROR_CODES = {
}

// exists primarily so we can test it without worrying about route params
export function ProjectCreateForm({ orgName }: { orgName: string }) {
const navigate = useNavigate()

const queryClient = useApiQueryClient()
const addToast = useToast()

export function ProjectCreateForm({
orgName,
onSuccess,
}: {
orgName: string
onSuccess: (p: Project) => void
}) {
const createProject = useApiMutation('organizationProjectsPost', {
onSuccess: (data) => {
// refetch list of projects in sidebar
queryClient.invalidateQueries('organizationProjectsGet', {
organizationName: orgName,
})
// avoid the project fetch when the project page loads since we have the data
queryClient.setQueryData(
'organizationProjectsGetProject',
{ organizationName: orgName, projectName: data.name },
data
)
addToast({
icon: <Success16Icon />,
title: 'Success!',
content: 'Your project has been created.',
timeout: 5000,
})
navigate(`/orgs/${orgName}/projects/${data.name}`)
},
onSuccess,
})
return (
<Formik
initialValues={{ name: '', description: '' }}
onSubmit={({ name, description }) => {
createProject.mutate({
organizationName: orgName,
projectCreate: { name, description },
body: { name, description },
})
}}
>
Expand Down Expand Up @@ -105,6 +89,10 @@ export function ProjectCreateForm({ orgName }: { orgName: string }) {
}

export default function ProjectCreatePage() {
const queryClient = useApiQueryClient()
const addToast = useToast()
const navigate = useNavigate()

const { orgName } = useParams('orgName')
return (
<>
Expand All @@ -113,7 +101,28 @@ export default function ProjectCreatePage() {
Create a new project
</PageTitle>
</PageHeader>
<ProjectCreateForm orgName={orgName} />
<ProjectCreateForm
orgName={orgName}
onSuccess={(project) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a function that takes the created project is a surprisingly natural interface here

// refetch list of projects in sidebar
queryClient.invalidateQueries('organizationProjectsGet', {
organizationName: orgName,
})
// avoid the project fetch when the project page loads since we have the data
queryClient.setQueryData(
'organizationProjectsGetProject',
{ organizationName: orgName, projectName: project.name },
project
)
addToast({
icon: <Success16Icon />,
title: 'Success!',
content: 'Your project has been created.',
timeout: 5000,
})
navigate(`../${project.name}`)
}}
/>
</>
)
}
2 changes: 1 addition & 1 deletion app/pages/ProjectsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const ProjectsPage = () => {
</section>
<footer className="p-4 border-t border-gray-400 text-xs">
<span className="uppercase">
{formatDistanceToNowStrict(item.timeCreated, {
{formatDistanceToNowStrict(new Date(item.timeCreated), {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't parse dates by default. we fix this on the generator side by automatically parsing timeCreated and timeModified fields (or maybe any field that starts with time) as dates.

addSuffix: true,
})}
</span>
Expand Down
21 changes: 15 additions & 6 deletions app/pages/__tests__/InstanceCreateForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,20 @@ const instancesUrl = `${projectUrl}/instances`
const disksUrl = `${projectUrl}/disks`
const vpcsUrl = `${projectUrl}/vpcs`

let successSpy: jest.Mock

describe('InstanceCreateForm', () => {
beforeEach(() => {
// existing disk modal fetches disks on render even if it's not visible
fetchMock.get(disksUrl, 200)
fetchMock.get(vpcsUrl, 200)
successSpy = jest.fn()
renderWithRouter(
<InstanceCreateForm orgName={org.name} projectName={project.name} />
<InstanceCreateForm
orgName={org.name}
projectName={project.name}
onSuccess={successSpy}
/>
)
})

Expand Down Expand Up @@ -62,7 +69,10 @@ describe('InstanceCreateForm', () => {
})

it('shows generic message for unknown server error', async () => {
fetchMock.post(instancesUrl, 400)
fetchMock.post(instancesUrl, {
status: 400,
body: { error_code: 'UnknownCode' },
})

fireEvent.click(submitButton())

Expand All @@ -89,16 +99,15 @@ describe('InstanceCreateForm', () => {
)
})

it('navigates to project page on success', async () => {
it('calls onSuccess on success', async () => {
const mock = fetchMock.post(instancesUrl, { status: 201, body: instance })

const projectPath = `/orgs/${org.name}/projects/${project.name}`
expect(window.location.pathname).not.toEqual(projectPath)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was never a good way to do this anyway, so I don't miss it. the problem was the location was being set by a previously run test, so this not equal was failing

expect(successSpy).not.toHaveBeenCalled()

fireEvent.click(submitButton())

await waitFor(() => expect(mock.called(instancesUrl)).toBeTruthy())
await waitFor(() => expect(mock.done()).toBeTruthy())
await waitFor(() => expect(window.location.pathname).toEqual(projectPath))
await waitFor(() => expect(successSpy).toHaveBeenCalled())
})
})
19 changes: 13 additions & 6 deletions app/pages/__tests__/ProjectCreatePage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ function enterName(value: string) {
fireEvent.change(nameInput, { target: { value } })
}

let successSpy: jest.Mock

describe('ProjectCreateForm', () => {
beforeEach(() => {
renderWithRouter(<ProjectCreateForm orgName={org.name} />)
successSpy = jest.fn()
renderWithRouter(
<ProjectCreateForm orgName={org.name} onSuccess={successSpy} />
)
enterName('valid-name')
})

Expand Down Expand Up @@ -78,7 +83,10 @@ describe('ProjectCreateForm', () => {
})

it('shows generic message for unknown server error', async () => {
fetchMock.post(projectsUrl, { status: 400 })
fetchMock.post(projectsUrl, {
status: 400,
body: { error_code: 'UnknownCode' },
})

fireEvent.click(submitButton())

Expand All @@ -95,19 +103,18 @@ describe('ProjectCreateForm', () => {
)
})

it('navigates to project page on success', async () => {
it('calls onSuccess on success', async () => {
const mock = fetchMock.post(projectsUrl, {
status: 201,
body: project,
})

const projectPath = `/orgs/${org.name}/projects/${project.name}`
expect(window.location.pathname).not.toEqual(projectPath)
expect(successSpy).not.toHaveBeenCalled()

fireEvent.click(submitButton())

await waitFor(() => expect(mock.called()).toBeTruthy())
await waitFor(() => expect(mock.done()).toBeTruthy())
await waitFor(() => expect(window.location.pathname).toEqual(projectPath))
await waitFor(() => expect(successSpy).toHaveBeenCalled())
})
})
19 changes: 10 additions & 9 deletions app/pages/project/instances/create/InstancesCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,20 @@ const ERROR_CODES = {
type InstanceCreateFormProps = {
orgName: string
projectName: string
onSuccess: () => void
}

export function InstanceCreateForm({
orgName,
projectName,
onSuccess,
}: InstanceCreateFormProps) {
const navigate = useNavigate()

// modals
const [showNewDiskModal, setShowNewDiskModal] = useState(false)
const [showExistingDiskModal, setShowExistingDiskModal] = useState(false)
const [showNetworkModal, setShowNetworkModal] = useState(false)

const createInstance = useApiMutation('projectInstancesPost', {
onSuccess: () => {
navigate(`/orgs/${orgName}/projects/${projectName}`)
},
})
const createInstance = useApiMutation('projectInstancesPost', { onSuccess })

const renderLargeRadioCards = (category: string) => {
return INSTANCE_SIZES.filter((option) => option.category === category).map(
Expand Down Expand Up @@ -95,7 +91,7 @@ export function InstanceCreateForm({
createInstance.mutate({
organizationName: orgName,
projectName,
instanceCreate: {
body: {
name: values['instance-name'],
hostname: values.hostname,
description: `An instance in project: ${projectName}`,
Expand Down Expand Up @@ -336,6 +332,7 @@ export function InstanceCreateForm({
}

const InstanceCreatePage = () => {
const navigate = useNavigate()
const { orgName, projectName } = useParams('orgName', 'projectName')

return (
Expand All @@ -345,7 +342,11 @@ const InstanceCreatePage = () => {
Create a new instance
</PageTitle>
</PageHeader>
<InstanceCreateForm orgName={orgName} projectName={projectName} />
<InstanceCreateForm
orgName={orgName}
projectName={projectName}
onSuccess={() => navigate('..')} // project page
/>
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const VpcSubnetsTab = () => {
<Column
id="ip-block"
header="IP Block"
accessor={(vpc) => [vpc.ipv4Block, vpc.ipv6Block]}
accessor={(vpc) => [vpc.ipv4_block, vpc.ipv6_block]}
cell={TwoLineCell}
/>
<Column id="created" accessor="identity.timeCreated" cell={DateCell} />
Expand Down
Loading