Skip to content

Commit

Permalink
Merge branch 'main' into client-authn
Browse files Browse the repository at this point in the history
# Conflicts:
#	OMICRON_VERSION
  • Loading branch information
david-crespo committed Jun 29, 2022
2 parents 071a51e + 1f746d5 commit 67a43bb
Show file tree
Hide file tree
Showing 132 changed files with 6,646 additions and 1,682 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/upload-assets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
cache: 'yarn'
- run: yarn install
- name: Build for Nexus
run: yarn build-for-nexus
run: SHA=${{ github.sha }} yarn build-for-nexus
- run: mkdir -p releases/console
- name: Make <sha>.tar.gz
run: tar czf releases/console/${{ github.sha }}.tar.gz --directory=dist .
Expand Down
2 changes: 1 addition & 1 deletion OMICRON_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
af1049c5f72cc91c4ce89650b74d92c9e26484a6
154a4a6cd623497cbe893c2cd3ea1c241516bc21
25 changes: 14 additions & 11 deletions app/components/Breadcrumbs.e2e.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ type Crumb = {

async function expectCrumbs(page: Page, crumbs: Crumb[]) {
const crumbsInPage = page.locator(`data-testid=Breadcrumbs >> role=listitem`)
await expect(crumbsInPage).toHaveCount(crumbs.length)
// crumbs should be shorter by one
// since that is the page we are already on
await expect(crumbsInPage).toHaveCount(Math.max(crumbs.length - 1, 0)) // unmatched route should = 0 not -1

for (let i = 0; i < crumbs.length; i++) {
for (let i = 0; i < crumbs.length - 1; i++) {
const { text, href } = crumbs[i]
const listItem = crumbsInPage.nth(i)
await expect(listItem).toHaveText(text)
Expand All @@ -23,19 +25,15 @@ async function expectCrumbs(page: Page, crumbs: Crumb[]) {
}
}

async function expectTitle(page: Page, title: string) {
expect(await page.title()).toEqual(title)
}

test.describe('Breadcrumbs', () => {
test('not present on unmatched route', async ({ page }) => {
await page.goto('/abc/def')
await expectCrumbs(page, [])
})

test('work on new project', async ({ page }) => {
await page.goto('/orgs/maze-war/projects/new')
await expectCrumbs(page, [
{ text: 'maze-war', href: '/orgs/maze-war' },
{ text: 'Projects', href: '/orgs/maze-war/projects' },
{ text: 'Create project' },
])
await expectTitle(page, 'Oxide Console')
})

test('works on VPC detail', async ({ page }) => {
Expand All @@ -53,5 +51,10 @@ test.describe('Breadcrumbs', () => {
},
{ text: 'mock-vpc' },
])

await expectTitle(
page,
'mock-vpc / VPCs / mock-project / Projects / maze-war / Oxide Console'
)
})
})
24 changes: 20 additions & 4 deletions app/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from 'react'
import { useMatches } from 'react-router-dom'
import invariant from 'tiny-invariant'
import type { Merge, SetRequired } from 'type-fest'
Expand Down Expand Up @@ -26,12 +27,27 @@ const useCrumbs = () =>
// hasCrumb could be inline (m) => m.handle?.crumb, but it's extracted so we can
// give it a guard type so the typing is nice around filter()
.filter(hasCrumb)
.map((m, i, arr) => ({
.map((m) => ({
label: typeof m.handle.crumb === 'function' ? m.handle.crumb(m) : m.handle.crumb,
// last one is the page we're on, so no link
href: i < arr.length - 1 ? m.pathname : undefined,
href: m.pathname,
}))

export function Breadcrumbs() {
return <BreadcrumbsPure data={useCrumbs()} />
const crumbs = useCrumbs()
// output
// non top-level route: Instances / mock-project / Projects / maze-war / Oxide Console
// top-level route: Oxide Console
const title = crumbs
.slice() // avoid mutating original with reverse()
.reverse()
.map((item) => item.label)
.concat('Oxide Console')
.join(' / ')

useEffect(() => {
document.title = title
}, [title])

// remove last crumb which is the page we are on
return <BreadcrumbsPure data={crumbs.slice(0, -1)} />
}
19 changes: 5 additions & 14 deletions app/components/FormPage.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import React, { Suspense } from 'react'
import { useNavigate } from 'react-router-dom'

import { PageHeader, PageTitle } from '@oxide/ui'

interface FormPageProps {
Form: React.ComponentType<{
onSuccess: (data: { name: string }) => void
}>
// TODO: This obviously shouldn't be any, but the lower form types are fucked
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Form: React.ComponentType<any>
goToCreatedPage?: boolean
title: string
icon?: React.ReactElement
}

export function FormPage({ Form, title, icon, goToCreatedPage = true }: FormPageProps) {
export function FormPage({ Form, goToCreatedPage = true }: FormPageProps) {
const navigate = useNavigate()
return (
// TODO: Add a proper loading state
<Suspense fallback={null}>
{title && (
<PageHeader>
<PageTitle icon={icon}>{title}</PageTitle>
</PageHeader>
)}
<Form
onSuccess={(data) =>
onSuccess={(data: { name: string }) =>
goToCreatedPage ? navigate(`../${data.name}`) : navigate('..')
}
/>
Expand Down
14 changes: 11 additions & 3 deletions app/components/MoreActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ interface MoreActionsMenuProps {
export const MoreActionsMenu = ({ actions, label }: MoreActionsMenuProps) => {
return (
<Menu>
<MenuButton aria-label={label}>
<MenuButton
aria-label={label}
className="w-6 h-6 border border-default flex rounded items-center justify-center hover:bg-hover"
>
<More12Icon className="text-tertiary" />
</MenuButton>
<MenuList>
<MenuList className="mt-2">
{actions.map((a) => (
<MenuItem disabled={a.disabled} key={a.label} onSelect={a.onActivate}>
<MenuItem
className={a.className}
disabled={a.disabled}
key={a.label}
onSelect={a.onActivate}
>
{a.label}
</MenuItem>
))}
Expand Down
6 changes: 6 additions & 0 deletions app/components/PageActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { tunnel } from '@oxide/util'

const Tunnel = tunnel('page-actions')

export const PageActions = Tunnel.In
export const PageActionsTarget = Tunnel.Out
70 changes: 54 additions & 16 deletions app/components/ProjectSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import cn from 'classnames'
import { useParams as useRRParams } from 'react-router-dom'
import { Menu, MenuButton, MenuItem, MenuLink, MenuList } from '@reach/menu-button'
import { Link } from 'react-router-dom'

import { useApiQuery } from '@oxide/api'
import { SelectArrows6Icon } from '@oxide/ui'

import { useParams } from 'app/hooks'

/**
* This is mostly temporary until we figure out the proper thing to go here
*/
Expand All @@ -18,21 +21,56 @@ const BrandIcon = () => (
</svg>
)

interface ProjectSelectorProps {
className?: string
}
export const ProjectSelector = ({ className }: ProjectSelectorProps) => {
const { orgName, projectName } = useRRParams()
export const ProjectSelector = () => {
const { orgName, projectName } = useParams('orgName')

const { data } = useApiQuery('organizationProjectsGet', { orgName, limit: 20 })

// filter out current project if there is one. if there isn't one, it'll be
// undefined and it won't match any
const projects = (data?.items || []).filter((p) => p.name !== projectName)

return (
<div className={cn('mt-1 flex items-center justify-between', className)}>
<div className="flex items-center">
<BrandIcon />
<div className="ml-2 pb-0.5 leading-4 text-sans-sm">
<div>{orgName}</div>
<div className="text-secondary">{projectName || 'select a project'}</div>
<Menu>
<MenuButton
aria-label="Switch project"
className="mt-1 flex items-center justify-between w-full group"
>
<div className="flex items-center">
<BrandIcon />
<div className="ml-2 pb-0.5 leading-4 text-sans-sm text-left">
<div>{orgName}</div>
<div className="text-secondary w-[140px] text-ellipsis whitespace-nowrap overflow-hidden">
{projectName || 'select a project'}
</div>
</div>
</div>
{/* aria-hidden is a tip from the Reach docs */}
<div className="flex flex-shrink-0 w-[1.125rem] h-[1.625rem] rounded border border-secondary justify-center items-center group-hover:bg-hover">
<SelectArrows6Icon className="text-secondary" aria-hidden />
</div>
</div>
<SelectArrows6Icon className="text-secondary" />
</div>
</MenuButton>
<MenuList className="mt-2">
{projects.length > 0 ? (
projects.map((project) => (
<MenuLink
key={project.name}
as={Link}
to={`/orgs/${orgName}/projects/${project.name}`}
>
{project.name}
</MenuLink>
))
) : (
<MenuItem
className="!text-center hover:cursor-default !pr-3 !text-secondary"
onSelect={() => {}}
disabled
>
No other projects found
</MenuItem>
)}
</MenuList>
</Menu>
)
}
40 changes: 23 additions & 17 deletions app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import cn from 'classnames'
import { NavLink as RRNavLink, useLocation } from 'react-router-dom'
import { NavLink as RRNavLink } from 'react-router-dom'

import { Document16Icon, Settings16Icon } from '@oxide/ui'
import { Document16Icon } from '@oxide/ui'
import { flattenChildren, pluckFirstOfType } from '@oxide/util'

import { ProjectSelector } from 'app/components/ProjectSelector'

interface SidebarProps {
children: React.ReactNode
}
export function Sidebar({ children }: SidebarProps) {
const { pathname } = useLocation()
const childArray = flattenChildren(children)
const projectSelector = pluckFirstOfType(childArray, ProjectSelector)

return (
<div className="ox-sidebar space-y-10 overflow-auto border-r px-3 pb-6 pt-5 text-sans-md text-default border-secondary">
{children}
<Sidebar.Footer>
<NavLinkItem to="https://docs.oxide.computer">
<Document16Icon /> Documentation
</NavLinkItem>
{!pathname.startsWith('/settings') && (
<NavLinkItem to="/settings">
<Settings16Icon /> Settings
<div className="ox-sidebar relative flex flex-col border-r px-3 pt-5 text-sans-md text-default border-secondary">
{projectSelector}
<div className="overflow-y-auto mt-10 flex flex-col flex-grow">
{childArray}
<Sidebar.Footer>
<NavLinkItem to="https://docs.oxide.computer">
<Document16Icon /> Documentation
</NavLinkItem>
)}
</Sidebar.Footer>
</Sidebar.Footer>
</div>
</div>
)
}
Expand All @@ -31,7 +34,7 @@ interface SidebarNav {
}
Sidebar.Nav = ({ children, heading }: SidebarNav) => {
return (
<div className="mt-8 space-y-1">
<div className="ox-sidebar-nav space-y-1">
{heading ? <span className="ml-2 text-mono-sm text-secondary">{heading}</span> : null}
<nav>
<ul className="space-y-0.5">{children}</ul>
Expand All @@ -45,8 +48,11 @@ interface SidebarFooter {
}
Sidebar.Footer = ({ children }: SidebarFooter) => {
return (
// TODO: The `w-[12.5rem] is hand calculated and very likely isn't what we want. Do something better here
<ul className="absolute bottom-0 w-[12.5rem] space-y-0.5 pb-3">{children}</ul>
<ul className="ox-sidebar-footer w-full pb-3">
<span className="heading hidden ml-2 text-mono-sm text-secondary">More</span>

<div>{children}</div>
</ul>
)
}

Expand Down
4 changes: 2 additions & 2 deletions app/components/StatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const InstanceStatusBadge = (props: {
status: InstanceState
className?: string
}) => (
<Badge variant="secondary" {...INSTANCE_COLORS[props.status]} className={props.className}>
<Badge variant="default" {...INSTANCE_COLORS[props.status]} className={props.className}>
{props.status}
</Badge>
)
Expand All @@ -37,7 +37,7 @@ const DISK_COLORS: Record<DiskStateStr, BadgeColor> = {
}

export const DiskStatusBadge = (props: { status: DiskStateStr; className?: string }) => (
<Badge variant="secondary" color={DISK_COLORS[props.status]} className={props.className}>
<Badge variant="default" color={DISK_COLORS[props.status]} className={props.className}>
{props.status}
</Badge>
)
Loading

0 comments on commit 67a43bb

Please sign in to comment.