Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: Update routes for project page, workspace creation page, and workspace page #415

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 5 commits into from
Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 site/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface Workspace {

export namespace Workspace {
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
const response = await fetch(`/api/v2/workspaces/me`, {
const response = await fetch(`/api/v2/users/me/workspaces`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
4 changes: 2 additions & 2 deletions site/components/Workspace/Workspace.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { render, screen } from "@testing-library/react"
import React from "react"
import { Workspace } from "./Workspace"
import { MockWorkspace } from "../../test_helpers"
import { MockOrganization, MockProject, MockWorkspace } from "../../test_helpers"

describe("Workspace", () => {
it("renders", async () => {
// When
render(<Workspace workspace={MockWorkspace} />)
render(<Workspace organization={MockOrganization} project={MockProject} workspace={MockWorkspace} />)

// Then
const element = await screen.findByText(MockWorkspace.name)
Expand Down
12 changes: 8 additions & 4 deletions site/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ import * as API from "../../api"
import { WorkspaceSection } from "./WorkspaceSection"

export interface WorkspaceProps {
organization: API.Organization
workspace: API.Workspace
project: API.Project
}

/**
* Workspace is the top-level component for viewing an individual workspace
*/
export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
export const Workspace: React.FC<WorkspaceProps> = ({ organization, project, workspace }) => {
const styles = useStyles()

return (
<div className={styles.root}>
<div className={styles.vertical}>
<WorkspaceHeader workspace={workspace} />
<WorkspaceHeader organization={organization} project={project} workspace={workspace} />
<div className={styles.horizontal}>
<div className={styles.sidebarContainer}>
<WorkspaceSection title="Applications">
Expand Down Expand Up @@ -54,17 +56,19 @@ export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
/**
* Component for the header at the top of the workspace page
*/
export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ workspace }) => {
export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ organization, project, workspace }) => {
const styles = useStyles()

const projectLink = `/projects/${organization.name}/${project.name}`

return (
<Paper elevation={0} className={styles.section}>
<div className={styles.horizontal}>
<WorkspaceHeroIcon />
<div className={styles.vertical}>
<Typography variant="h4">{workspace.name}</Typography>
<Typography variant="body2" color="textSecondary">
<Link href="javascript:;">{workspace.project_id}</Link>
<Link href={projectLink}>{project.name}</Link>
</Typography>
</div>
</div>
Expand Down
22 changes: 16 additions & 6 deletions site/pages/projects/[organization]/[project]/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,36 @@ import { useUser } from "../../../../contexts/UserContext"
import { ErrorSummary } from "../../../../components/ErrorSummary"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm"
import { unsafeSWRArgument } from "../../../../util"
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious what this is, but I'm sure you have a good reason for using it!

Copy link
Contributor Author

@bryphe-coder bryphe-coder Mar 9, 2022

Choose a reason for hiding this comment

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

Good question @presleyp ! I'm open to ideas for better ways to handle this (and luckily, it's at least temporary - if we switch to XState and migrate from SWR, this utility isn't needed).

With SWR - when one call relies on another call - SWR expects the function to 'throw' until the data is available. So I ended up with a lot of casts to any + suppressions:

  const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
    // Using `any` below so that, if the project isn't available, an exception is thrown - causing
    // SWR to retry:
    // eslint-disable-line no-any
    return `/api/v2/organizations/${(project as any).id}/projects/${projectName}`
  })

I ended up with a lot of these comments + any casts, so I figured if I extracted it out to a helper, it'd be a little clearer:

  const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
    return `/api/v2/organizations/${unsafeSWRArgument(project).id}/projects/${projectName}`
  })

I'm happy to switch back to the more verbose comment + eslint disable + cast, or rename unsafeSWRArgument to something more understandable. (Implementation is here if it helps) IMO the most important criteria is that it is readable / understandable for you and @vapurrmaid

And luckily, seems like this will be only temporary - if we remove SWR and use XState, this will no longer be an issue and this can be removed

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see where you defined! (Thought it was an SWR thing at first)


const CreateWorkspacePage: React.FC = () => {
const { push, query } = useRouter()
const styles = useStyles()
const { me } = useUser(/* redirectOnError */ true)
const { organization, project: projectName } = query
const { data: project, error: projectError } = useSWR<API.Project, Error>(
`/api/v2/projects/${organization}/${projectName}`,
const { organization: organizationName, project: projectName } = query

const { data: organizationInfo, error: organizationError } = useSWR<API.Organization, Error>(
() => `/api/v2/users/me/organizations/${organizationName}`,
)

const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
return `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}`
})

const onCancel = useCallback(async () => {
await push(`/projects/${organization}/${projectName}`)
}, [push, organization, projectName])
await push(`/projects/${organizationName}/${projectName}`)
}, [push, organizationName, projectName])

const onSubmit = async (req: API.CreateWorkspaceRequest) => {
const workspace = await API.Workspace.create(req)
await push(`/workspaces/me/${workspace.name}`)
await push(`/workspaces/${workspace.id}`)
return workspace
}

if (organizationError) {
return <ErrorSummary error={organizationError} />
}

if (projectError) {
return <ErrorSummary error={projectError} />
}
Expand Down
41 changes: 28 additions & 13 deletions site/pages/projects/[organization]/[project]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Link from "next/link"
import { useRouter } from "next/router"
import useSWR from "swr"

import { Project, Workspace } from "../../../../api"
import { Organization, Project, Workspace } from "../../../../api"
import { Header } from "../../../../components/Header"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { Navbar } from "../../../../components/Navbar"
Expand All @@ -15,21 +15,32 @@ import { useUser } from "../../../../contexts/UserContext"
import { ErrorSummary } from "../../../../components/ErrorSummary"
import { firstOrItem } from "../../../../util/array"
import { EmptyState } from "../../../../components/EmptyState"
import { unsafeSWRArgument } from "../../../../util"

const ProjectPage: React.FC = () => {
const styles = useStyles()
const { me, signOut } = useUser(true)

const router = useRouter()
const { project, organization } = router.query
const { project: projectName, organization: organizationName } = router.query

const { data: projectInfo, error: projectError } = useSWR<Project, Error>(
() => `/api/v2/projects/${organization}/${project}`,
const { data: organizationInfo, error: organizationError } = useSWR<Organization, Error>(
() => `/api/v2/users/me/organizations/${organizationName}`,
)
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(
() => `/api/v2/projects/${organization}/${project}/workspaces`,

const { data: projectInfo, error: projectError } = useSWR<Project, Error>(
() => `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}`,
)

// TODO: The workspaces endpoint was recently changed, so that we can't get
// workspaces per-project. This just grabs all workspaces... and then
// later filters them to match the current project.
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(() => `/api/v2/users/me/workspaces`)

if (organizationError) {
return <ErrorSummary error={organizationError} />
}

if (projectError) {
return <ErrorSummary error={projectError} />
}
Expand All @@ -43,7 +54,7 @@ const ProjectPage: React.FC = () => {
}

const createWorkspace = () => {
void router.push(`/projects/${organization}/${project}/create`)
void router.push(`/projects/${organizationName}/${projectName}/create`)
}

const emptyState = (
Expand All @@ -61,26 +72,30 @@ const ProjectPage: React.FC = () => {
{
key: "name",
name: "Name",
renderer: (nameField: string) => {
return <Link href={`/workspaces/me/${nameField}`}>{nameField}</Link>
renderer: (nameField: string, workspace: Workspace) => {
return <Link href={`/workspaces/${workspace.id}`}>{nameField}</Link>
},
},
]

const perProjectWorkspaces = workspaces.filter((workspace) => {
return workspace.project_id === projectInfo.id
})

const tableProps = {
title: "Workspaces",
columns,
data: workspaces,
data: perProjectWorkspaces,
emptyState: emptyState,
}

return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />
<Header
title={firstOrItem(project, "")}
description={firstOrItem(organization, "")}
subTitle={`${workspaces.length} workspaces`}
title={firstOrItem(projectName, "")}
description={firstOrItem(organizationName, "")}
subTitle={`${perProjectWorkspaces.length} workspaces`}
action={{
text: "Create Workspace",
onClick: createWorkspace,
Expand Down
71 changes: 0 additions & 71 deletions site/pages/workspaces/[user]/[workspace].tsx

This file was deleted.

78 changes: 78 additions & 0 deletions site/pages/workspaces/[workspace].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from "react"
import useSWR from "swr"
import { makeStyles } from "@material-ui/core/styles"
import { useRouter } from "next/router"
import { Navbar } from "../../components/Navbar"
import { Footer } from "../../components/Page"
import { useUser } from "../../contexts/UserContext"
import { firstOrItem } from "../../util/array"
import { ErrorSummary } from "../../components/ErrorSummary"
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
import { Workspace } from "../../components/Workspace"
import { unsafeSWRArgument } from "../../util"
import * as API from "../../api"

const WorkspacesPage: React.FC = () => {
const styles = useStyles()
const router = useRouter()
const { me, signOut } = useUser(true)

const { workspace: workspaceQueryParam } = router.query

const { data: workspace, error: workspaceError } = useSWR<API.Workspace, Error>(() => {
const workspaceParam = firstOrItem(workspaceQueryParam, null)

return `/api/v2/workspaces/${workspaceParam}`
})

// Fetch parent project
const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
return `/api/v2/projects/${unsafeSWRArgument(workspace).project_id}`
})

const { data: organization, error: organizationError } = useSWR<API.Project, Error>(() => {
return `/api/v2/organizations/${unsafeSWRArgument(project).organization_id}`
})

if (workspaceError) {
return <ErrorSummary error={workspaceError} />
}

if (projectError) {
return <ErrorSummary error={projectError} />
}

if (organizationError) {
return <ErrorSummary error={organizationError} />
}

if (!me || !workspace || !project || !organization) {
return <FullScreenLoader />
}

return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />

<div className={styles.inner}>
<Workspace organization={organization} project={project} workspace={workspace} />
</div>

<Footer />
</div>
)
}

const useStyles = makeStyles(() => ({
root: {
display: "flex",
flexDirection: "column",
},
inner: {
maxWidth: "1380px",
margin: "1em auto",
width: "100%",
},
}))

export default WorkspacesPage
2 changes: 2 additions & 0 deletions site/util/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./array"
export * from "./swr"
17 changes: 17 additions & 0 deletions site/util/swr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* unsafeSWRArgument
*
* Helper function for working with SWR / useSWR in the TypeScript world.
* TypeScript is helpful in enforcing type-safety, but SWR is designed to
* with the expectation that, if the argument is not available, an exception
* will be thrown.
*
* This just helps in abiding by those rules, explicitly, and lets us suppress
* the lint warning in a single place.
*/
export const unsafeSWRArgument = <T>(arg: T | null | undefined): T => {
if (typeof arg === "undefined" || arg === null) {
throw "SWR: Expected exception because the argument is not available"
}
return arg
}