diff --git a/.github/workflows/upload-assets.yaml b/.github/workflows/upload-assets.yaml index 8b2dfadb9..d3e81ab2c 100644 --- a/.github/workflows/upload-assets.yaml +++ b/.github/workflows/upload-assets.yaml @@ -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 .tar.gz run: tar czf releases/console/${{ github.sha }}.tar.gz --directory=dist . diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 40116ffe3..e162c681d 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -af1049c5f72cc91c4ce89650b74d92c9e26484a6 \ No newline at end of file +154a4a6cd623497cbe893c2cd3ea1c241516bc21 \ No newline at end of file diff --git a/app/components/Breadcrumbs.e2e.tsx b/app/components/Breadcrumbs.e2e.tsx index 245edc466..47a8152da 100644 --- a/app/components/Breadcrumbs.e2e.tsx +++ b/app/components/Breadcrumbs.e2e.tsx @@ -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) @@ -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 }) => { @@ -53,5 +51,10 @@ test.describe('Breadcrumbs', () => { }, { text: 'mock-vpc' }, ]) + + await expectTitle( + page, + 'mock-vpc / VPCs / mock-project / Projects / maze-war / Oxide Console' + ) }) }) diff --git a/app/components/Breadcrumbs.tsx b/app/components/Breadcrumbs.tsx index dd41210d0..a4a57423b 100644 --- a/app/components/Breadcrumbs.tsx +++ b/app/components/Breadcrumbs.tsx @@ -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' @@ -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 + 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 } diff --git a/app/components/FormPage.tsx b/app/components/FormPage.tsx index 727bb2742..ec91740ca 100644 --- a/app/components/FormPage.tsx +++ b/app/components/FormPage.tsx @@ -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 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 - {title && ( - - {title} - - )}
+ onSuccess={(data: { name: string }) => goToCreatedPage ? navigate(`../${data.name}`) : navigate('..') } /> diff --git a/app/components/MoreActionsMenu.tsx b/app/components/MoreActionsMenu.tsx index fdabe1564..349616ded 100644 --- a/app/components/MoreActionsMenu.tsx +++ b/app/components/MoreActionsMenu.tsx @@ -11,12 +11,20 @@ interface MoreActionsMenuProps { export const MoreActionsMenu = ({ actions, label }: MoreActionsMenuProps) => { return ( - + - + {actions.map((a) => ( - + {a.label} ))} diff --git a/app/components/PageActions.tsx b/app/components/PageActions.tsx new file mode 100644 index 000000000..5d3eecfd1 --- /dev/null +++ b/app/components/PageActions.tsx @@ -0,0 +1,6 @@ +import { tunnel } from '@oxide/util' + +const Tunnel = tunnel('page-actions') + +export const PageActions = Tunnel.In +export const PageActionsTarget = Tunnel.Out diff --git a/app/components/ProjectSelector.tsx b/app/components/ProjectSelector.tsx index 094a1a8a1..344ecb86d 100644 --- a/app/components/ProjectSelector.tsx +++ b/app/components/ProjectSelector.tsx @@ -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 */ @@ -18,21 +21,56 @@ const BrandIcon = () => ( ) -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 ( -
-
- -
-
{orgName}
-
{projectName || 'select a project'}
+ + +
+ +
+
{orgName}
+
+ {projectName || 'select a project'} +
+
+
+ {/* aria-hidden is a tip from the Reach docs */} +
+
-
- -
+ + + {projects.length > 0 ? ( + projects.map((project) => ( + + {project.name} + + )) + ) : ( + {}} + disabled + > + No other projects found + + )} + +
) } diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index 7fb494df9..6435a5eb3 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -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 ( -
- {children} - - - Documentation - - {!pathname.startsWith('/settings') && ( - - Settings +
+ {projectSelector} +
+ {childArray} + + + Documentation - )} - + +
) } @@ -31,7 +34,7 @@ interface SidebarNav { } Sidebar.Nav = ({ children, heading }: SidebarNav) => { return ( -
+
{heading ? {heading} : null}