diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx
index 9953c0533e..17e6113508 100644
--- a/site/.storybook/preview.jsx
+++ b/site/.storybook/preview.jsx
@@ -64,6 +64,14 @@ export const parameters = {
},
type: "tablet",
},
+ iphone12: {
+ name: "iPhone 12",
+ styles: {
+ height: "844px",
+ width: "390px",
+ },
+ type: "mobile",
+ },
terminal: {
name: "Terminal",
styles: {
diff --git a/site/src/components/Breadcrumb/Breadcrumb.tsx b/site/src/components/Breadcrumb/Breadcrumb.tsx
index cd6625a42c..35f90d30a5 100644
--- a/site/src/components/Breadcrumb/Breadcrumb.tsx
+++ b/site/src/components/Breadcrumb/Breadcrumb.tsx
@@ -28,7 +28,7 @@ export const BreadcrumbList = forwardRef<
= forwardRef<
- HTMLButtonElement,
- ButtonProps
->(({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button";
- return (
-
- );
-});
+export const Button = forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx
index 8008ea9d6c..c924317b20 100644
--- a/site/src/components/DropdownMenu/DropdownMenu.tsx
+++ b/site/src/components/DropdownMenu/DropdownMenu.tsx
@@ -7,10 +7,12 @@
*/
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
-import { Check, ChevronRight, Circle } from "lucide-react";
+import { Button } from "components/Button/Button";
+import { Check, ChevronDownIcon, ChevronRight, Circle } from "lucide-react";
import {
type ComponentPropsWithoutRef,
type ElementRef,
+ type FC,
type HTMLAttributes,
forwardRef,
} from "react";
@@ -196,7 +198,7 @@ export const DropdownMenuSeparator = forwardRef<
>(({ className, ...props }, ref) => (
));
diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx
index fb43291dd4..1aa749e83e 100644
--- a/site/src/contexts/ProxyContext.tsx
+++ b/site/src/contexts/ProxyContext.tsx
@@ -15,6 +15,8 @@ import {
import { useQuery } from "react-query";
import { type ProxyLatencyReport, useProxyLatency } from "./useProxyLatency";
+export type Proxies = readonly Region[] | readonly WorkspaceProxy[];
+export type ProxyLatencies = Record;
export interface ProxyContextValue {
// proxy is **always** the workspace proxy that should be used.
// The 'proxy.selectedProxy' field is the proxy being used and comes from either:
@@ -43,7 +45,7 @@ export interface ProxyContextValue {
// WorkspaceProxy[] is returned if the user is an admin. WorkspaceProxy extends Region with
// more information about the proxy and the status. More information includes the error message if
// the proxy is unhealthy.
- proxies?: readonly Region[] | readonly WorkspaceProxy[];
+ proxies?: Proxies;
// isFetched is true when the 'proxies' api call is complete.
isFetched: boolean;
isLoading: boolean;
@@ -51,7 +53,7 @@ export interface ProxyContextValue {
// proxyLatencies is a map of proxy id to latency report. If the proxyLatencies[proxy.id] is undefined
// then the latency has not been fetched yet. Calculations happen async for each proxy in the list.
// Refer to the returned report for a given proxy for more information.
- proxyLatencies: Record;
+ proxyLatencies: ProxyLatencies;
// refetchProxyLatencies will trigger refreshing of the proxy latencies. By default the latencies
// are loaded once.
refetchProxyLatencies: () => Date;
diff --git a/site/src/index.css b/site/src/index.css
index c97e827b98..5f690b5616 100644
--- a/site/src/index.css
+++ b/site/src/index.css
@@ -70,4 +70,17 @@
* {
@apply border-border;
}
+
+ /*
+ By default, Radix adds a margin to the `body` element when a dropdown is displayed,
+ causing some shifting when the dropdown has a full-width size, as is the case with the mobile menu.
+ To prevent this, we need to apply the styles below.
+
+ There’s a related issue on GitHub: Radix UI Primitives Issue #3251
+ https://github.com/radix-ui/primitives/issues/3251
+ */
+ html body[data-scroll-locked] {
+ --removed-body-scroll-bar-size: 0 !important;
+ margin-right: 0 !important;
+ }
}
diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx
index dc4b1b4d92..d1a75c02cd 100644
--- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx
+++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx
@@ -1,7 +1,6 @@
import { type Interpolation, type Theme, css, useTheme } from "@emotion/react";
-import Button from "@mui/material/Button";
import MenuItem from "@mui/material/MenuItem";
-import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
+import { Button } from "components/Button/Button";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import {
Popover,
@@ -9,6 +8,7 @@ import {
PopoverTrigger,
usePopover,
} from "components/deprecated/Popover/Popover";
+import { ChevronDownIcon } from "lucide-react";
import { linkToAuditing } from "modules/navigation";
import type { FC } from "react";
import { NavLink } from "react-router-dom";
@@ -16,7 +16,6 @@ import { NavLink } from "react-router-dom";
interface DeploymentDropdownProps {
canViewDeployment: boolean;
canViewOrganizations: boolean;
- canViewAllUsers: boolean;
canViewAuditLog: boolean;
canViewHealth: boolean;
}
@@ -24,7 +23,6 @@ interface DeploymentDropdownProps {
export const DeploymentDropdown: FC = ({
canViewDeployment,
canViewOrganizations,
- canViewAllUsers,
canViewAuditLog,
canViewHealth,
}) => {
@@ -34,7 +32,6 @@ export const DeploymentDropdown: FC = ({
!canViewAuditLog &&
!canViewOrganizations &&
!canViewDeployment &&
- !canViewAllUsers &&
!canViewHealth
) {
return null;
@@ -43,17 +40,9 @@ export const DeploymentDropdown: FC = ({
return (
-
- }
- >
+
@@ -70,7 +59,6 @@ export const DeploymentDropdown: FC = ({
diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx
new file mode 100644
index 0000000000..19c66c14b3
--- /dev/null
+++ b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx
@@ -0,0 +1,146 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { fn, userEvent, within } from "@storybook/test";
+import { PointerEventsCheckLevel } from "@testing-library/user-event";
+import type { FC } from "react";
+import { chromaticWithTablet } from "testHelpers/chromatic";
+import {
+ MockPrimaryWorkspaceProxy,
+ MockProxyLatencies,
+ MockSupportLinks,
+ MockUser,
+ MockUser2,
+ MockWorkspaceProxies,
+} from "testHelpers/entities";
+import { MobileMenu } from "./MobileMenu";
+
+const meta: Meta = {
+ title: "modules/dashboard/MobileMenu",
+ parameters: {
+ layout: "fullscreen",
+ viewport: {
+ defaultViewport: "iphone12",
+ },
+ },
+ component: MobileMenu,
+ args: {
+ proxyContextValue: {
+ proxy: {
+ preferredPathAppURL: "",
+ preferredWildcardHostname: "",
+ proxy: MockPrimaryWorkspaceProxy,
+ },
+ isLoading: false,
+ isFetched: true,
+ setProxy: fn(),
+ clearProxy: fn(),
+ refetchProxyLatencies: fn(),
+ proxyLatencies: MockProxyLatencies,
+ proxies: MockWorkspaceProxies,
+ },
+ user: MockUser,
+ supportLinks: MockSupportLinks,
+ docsHref: "https://coder.com/docs",
+ onSignOut: fn(),
+ isDefaultOpen: true,
+ canViewAuditLog: true,
+ canViewDeployment: true,
+ canViewHealth: true,
+ canViewOrganizations: true,
+ },
+ decorators: [withNavbarMock],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Closed: Story = {
+ args: {
+ isDefaultOpen: false,
+ },
+};
+
+export const Admin: Story = {
+ play: openAdminSettings,
+};
+
+export const Auditor: Story = {
+ args: {
+ user: MockUser2,
+ canViewAuditLog: true,
+ canViewDeployment: false,
+ canViewHealth: false,
+ canViewOrganizations: false,
+ },
+ play: openAdminSettings,
+};
+
+export const OrgAdmin: Story = {
+ args: {
+ user: MockUser2,
+ canViewAuditLog: true,
+ canViewDeployment: false,
+ canViewHealth: false,
+ canViewOrganizations: true,
+ },
+ play: openAdminSettings,
+};
+
+export const Member: Story = {
+ args: {
+ user: MockUser2,
+ canViewAuditLog: false,
+ canViewDeployment: false,
+ canViewHealth: false,
+ canViewOrganizations: false,
+ },
+};
+
+export const ProxySettings: Story = {
+ play: async ({ canvasElement }) => {
+ const user = setupUser();
+ const body = within(canvasElement.ownerDocument.body);
+ const menuItem = await body.findByRole("menuitem", {
+ name: /workspace proxy settings/i,
+ });
+ await user.click(menuItem);
+ },
+};
+
+export const UserSettings: Story = {
+ play: async ({ canvasElement }) => {
+ const user = setupUser();
+ const body = within(canvasElement.ownerDocument.body);
+ const menuItem = await body.findByRole("menuitem", {
+ name: /user settings/i,
+ });
+ await user.click(menuItem);
+ },
+};
+
+function withNavbarMock(Story: FC) {
+ return (
+
+
+
+ );
+}
+
+function setupUser() {
+ // It seems the dropdown component is disabling pointer events, which is
+ // causing Testing Library to throw an error. As a workaround, we can
+ // disable the pointer events check.
+ return userEvent.setup({
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
+ });
+}
+
+async function openAdminSettings({
+ canvasElement,
+}: { canvasElement: HTMLElement }) {
+ const user = setupUser();
+ const body = within(canvasElement.ownerDocument.body);
+ const menuItem = await body.findByRole("menuitem", {
+ name: /admin settings/i,
+ });
+ await user.click(menuItem);
+}
diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx
new file mode 100644
index 0000000000..e04bb7328d
--- /dev/null
+++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx
@@ -0,0 +1,339 @@
+import type * as TypesGen from "api/typesGenerated";
+import { Avatar } from "components/Avatar/Avatar";
+import { Button } from "components/Button/Button";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "components/Collapsible/Collapsible";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "components/DropdownMenu/DropdownMenu";
+import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
+import { displayError } from "components/GlobalSnackbar/utils";
+import { Latency } from "components/Latency/Latency";
+import type { ProxyContextValue } from "contexts/ProxyContext";
+import {
+ ChevronRightIcon,
+ CircleHelpIcon,
+ MenuIcon,
+ XIcon,
+} from "lucide-react";
+import { type FC, useState } from "react";
+import { Link } from "react-router-dom";
+import { cn } from "utils/cn";
+import { sortProxiesByLatency } from "./proxyUtils";
+
+const itemStyles = {
+ default: "px-9 h-10 no-underline",
+ sub: "pl-12",
+ open: "text-content-primary",
+};
+
+type MobileMenuPermissions = {
+ canViewDeployment: boolean;
+ canViewOrganizations: boolean;
+ canViewAuditLog: boolean;
+ canViewHealth: boolean;
+};
+
+type MobileMenuProps = MobileMenuPermissions & {
+ proxyContextValue?: ProxyContextValue;
+ user?: TypesGen.User;
+ supportLinks?: readonly TypesGen.LinkConfig[];
+ docsHref: string;
+ onSignOut: () => void;
+ isDefaultOpen?: boolean; // Useful for storybook
+};
+
+export const MobileMenu: FC = ({
+ isDefaultOpen,
+ proxyContextValue,
+ user,
+ supportLinks,
+ docsHref,
+ onSignOut,
+ ...permissions
+}) => {
+ const [open, setOpen] = useState(isDefaultOpen);
+ const hasSomePermission = Object.values(permissions).some((p) => p);
+
+ return (
+
+ {open && (
+
+ )}
+
+
+
+
+
+
+ {hasSomePermission && (
+ <>
+
+
+ >
+ )}
+
+
+
+ Docs
+
+
+
+
+
+
+ );
+};
+
+type ProxySettingsSubProps = {
+ proxyContextValue?: ProxyContextValue;
+};
+
+const ProxySettingsSub: FC = ({ proxyContextValue }) => {
+ const selectedProxy = proxyContextValue?.proxy.proxy;
+ const latency = selectedProxy
+ ? proxyContextValue?.proxyLatencies[selectedProxy?.id]
+ : undefined;
+ const [open, setOpen] = useState(false);
+
+ if (!selectedProxy) {
+ return null;
+ }
+
+ return (
+
+
+ {
+ e.preventDefault();
+ setOpen((prev) => !prev);
+ }}
+ >
+ Workspace proxy settings:
+
+
+ {latency && }
+
+
+
+
+
+ {proxyContextValue.proxies &&
+ sortProxiesByLatency(
+ proxyContextValue.proxies,
+ proxyContextValue.proxyLatencies,
+ ).map((p) => {
+ const latency = proxyContextValue.proxyLatencies[p.id];
+ return (
+ {
+ e.preventDefault();
+
+ if (!p.healthy) {
+ displayError("Please select a healthy workspace proxy.");
+ return;
+ }
+
+ proxyContextValue.setProxy(p);
+ setOpen(false);
+ }}
+ >
+
+ {p.display_name || p.name}
+ {latency ? (
+
+ ) : (
+
+ )}
+
+ );
+ })}
+
+
+ Proxy settings
+
+ {
+ proxyContextValue.refetchProxyLatencies();
+ }}
+ >
+ Refresh latencies
+
+
+
+ );
+};
+
+const AdminSettingsSub: FC = ({
+ canViewDeployment,
+ canViewOrganizations,
+ canViewAuditLog,
+ canViewHealth,
+}) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+ {
+ e.preventDefault();
+ setOpen((prev) => !prev);
+ }}
+ >
+ Admin settings
+
+
+
+
+ {canViewDeployment && (
+
+ Deployment
+
+ )}
+ {canViewOrganizations && (
+
+
+ Organizations
+
+
+
+ )}
+ {canViewAuditLog && (
+
+ Audit logs
+
+ )}
+ {canViewHealth && (
+
+ Healthcheck
+
+ )}
+
+
+ );
+};
+
+type UserSettingsSubProps = {
+ user?: TypesGen.User;
+ supportLinks?: readonly TypesGen.LinkConfig[];
+ onSignOut: () => void;
+};
+
+const UserSettingsSub: FC = ({
+ user,
+ supportLinks,
+ onSignOut,
+}) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+ {
+ e.preventDefault();
+ setOpen((prev) => !prev);
+ }}
+ >
+
+ User settings
+
+
+
+
+
+ Account
+
+
+ Sign out
+
+ {supportLinks && (
+ <>
+
+ {supportLinks?.map((l) => (
+
+
+ {l.name}
+
+
+ ))}
+ >
+ )}
+
+
+ );
+};
diff --git a/site/src/modules/dashboard/Navbar/Navbar.test.tsx b/site/src/modules/dashboard/Navbar/Navbar.test.tsx
index e01a1506f4..aa9a2c0400 100644
--- a/site/src/modules/dashboard/Navbar/Navbar.test.tsx
+++ b/site/src/modules/dashboard/Navbar/Navbar.test.tsx
@@ -7,7 +7,6 @@ import {
MockMemberPermissions,
} from "testHelpers/entities";
import { server } from "testHelpers/server";
-import { Language } from "./NavbarView";
/**
* The LicenseBanner, mounted above the AppRouter, fetches entitlements. Thus, to test their
@@ -24,13 +23,7 @@ describe("Navbar", () => {
render();
const deploymentMenu = await screen.findByText("Admin settings");
await userEvent.click(deploymentMenu);
- await waitFor(
- () => {
- const link = screen.getByText(Language.audit);
- expect(link).toBeDefined();
- },
- { timeout: 2000 },
- );
+ await screen.findByText("Audit Logs");
});
it("does not show Audit Log link when not entitled", async () => {
@@ -41,8 +34,7 @@ describe("Navbar", () => {
await userEvent.click(deploymentMenu);
await waitFor(
() => {
- const link = screen.queryByText(Language.audit);
- expect(link).toBe(null);
+ expect(screen.queryByText("Audit Logs")).not.toBeInTheDocument();
},
{ timeout: 2000 },
);
@@ -64,8 +56,7 @@ describe("Navbar", () => {
render();
await waitFor(
() => {
- const link = screen.queryByText("Deployment");
- expect(link).toBe(null);
+ expect(screen.queryByText("Deployment")).not.toBeInTheDocument();
},
{ timeout: 2000 },
);
diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx
index 5a3e86832e..5c3ccb72ff 100644
--- a/site/src/modules/dashboard/Navbar/Navbar.tsx
+++ b/site/src/modules/dashboard/Navbar/Navbar.tsx
@@ -20,7 +20,6 @@ export const Navbar: FC = () => {
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
const canViewOrganizations =
Boolean(permissions.editAnyOrganization) && showOrganizations;
- const canViewAllUsers = Boolean(permissions.viewAllUsers);
const proxyContextValue = useProxy();
const canViewHealth = canViewDeployment;
@@ -33,10 +32,10 @@ export const Navbar: FC = () => {
onSignOut={signOut}
canViewDeployment={canViewDeployment}
canViewOrganizations={canViewOrganizations}
- canViewAllUsers={canViewAllUsers}
canViewHealth={canViewHealth}
canViewAuditLog={canViewAuditLog}
proxyContextValue={proxyContextValue}
+ docsHref={appearance.docs_url}
/>
);
};
diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx
index 6ac0e51087..ae13c7fcc9 100644
--- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx
+++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx
@@ -11,7 +11,6 @@ const meta: Meta = {
component: NavbarView,
args: {
user: MockUser,
- canViewAllUsers: true,
canViewAuditLog: true,
canViewDeployment: true,
canViewHealth: true,
@@ -35,7 +34,6 @@ export const ForAdmin: Story = {
export const ForAuditor: Story = {
args: {
user: MockUser2,
- canViewAllUsers: false,
canViewAuditLog: true,
canViewDeployment: false,
canViewHealth: false,
@@ -52,7 +50,6 @@ export const ForAuditor: Story = {
export const ForOrgAdmin: Story = {
args: {
user: MockUser2,
- canViewAllUsers: false,
canViewAuditLog: true,
canViewDeployment: false,
canViewHealth: false,
@@ -69,7 +66,6 @@ export const ForOrgAdmin: Story = {
export const ForMember: Story = {
args: {
user: MockUser2,
- canViewAllUsers: false,
canViewAuditLog: false,
canViewDeployment: false,
canViewHealth: false,
diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx
index 3dd4251385..7b51561dde 100644
--- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx
+++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
import type { ProxyContextValue } from "contexts/ProxyContext";
import { MockPrimaryWorkspaceProxy, MockUser } from "testHelpers/entities";
import { renderWithAuth } from "testHelpers/renderHelpers";
-import { NavbarView, Language as navLanguage } from "./NavbarView";
+import { NavbarView } from "./NavbarView";
const proxyContextValue: ProxyContextValue = {
proxy: {
@@ -25,76 +25,75 @@ describe("NavbarView", () => {
it("workspaces nav link has the correct href", async () => {
renderWithAuth(
,
);
- const workspacesLink = await screen.findByText(navLanguage.workspaces);
- expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces");
+ const workspacesLink =
+ await screen.findByText(/workspaces/i);
+ expect(workspacesLink.href).toContain("/workspaces");
});
it("templates nav link has the correct href", async () => {
renderWithAuth(
,
);
- const templatesLink = await screen.findByText(navLanguage.templates);
- expect((templatesLink as HTMLAnchorElement).href).toContain("/templates");
+ const templatesLink =
+ await screen.findByText(/templates/i);
+ expect(templatesLink.href).toContain("/templates");
});
it("audit nav link has the correct href", async () => {
renderWithAuth(
,
);
const deploymentMenu = await screen.findByText("Admin settings");
await userEvent.click(deploymentMenu);
- const auditLink = await screen.findByText(navLanguage.audit);
- expect((auditLink as HTMLAnchorElement).href).toContain("/audit");
+ const auditLink = await screen.findByText(/audit logs/i);
+ expect(auditLink.href).toContain("/audit");
});
it("deployment nav link has the correct href", async () => {
renderWithAuth(
,
);
const deploymentMenu = await screen.findByText("Admin settings");
await userEvent.click(deploymentMenu);
- const deploymentSettingsLink = await screen.findByText(
- navLanguage.deployment,
- );
- expect((deploymentSettingsLink as HTMLAnchorElement).href).toContain(
- "/deployment/general",
- );
+ const deploymentSettingsLink =
+ await screen.findByText(/deployment/i);
+ expect(deploymentSettingsLink.href).toContain("/deployment/general");
});
});
diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx
index 662e39ca9d..ec3a1c690b 100644
--- a/site/src/modules/dashboard/Navbar/NavbarView.tsx
+++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx
@@ -1,38 +1,104 @@
-import { type Interpolation, type Theme, css, useTheme } from "@emotion/react";
-import MenuIcon from "@mui/icons-material/Menu";
-import Drawer from "@mui/material/Drawer";
-import IconButton from "@mui/material/IconButton";
import type * as TypesGen from "api/typesGenerated";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { CoderIcon } from "components/Icons/CoderIcon";
import type { ProxyContextValue } from "contexts/ProxyContext";
-import { type FC, useState } from "react";
+import type { FC } from "react";
import { NavLink, useLocation } from "react-router-dom";
-import { navHeight } from "theme/constants";
+import { cn } from "utils/cn";
import { DeploymentDropdown } from "./DeploymentDropdown";
+import { MobileMenu } from "./MobileMenu";
import { ProxyMenu } from "./ProxyMenu";
import { UserDropdown } from "./UserDropdown/UserDropdown";
export interface NavbarViewProps {
logo_url?: string;
user?: TypesGen.User;
+ docsHref: string;
buildInfo?: TypesGen.BuildInfoResponse;
supportLinks?: readonly TypesGen.LinkConfig[];
onSignOut: () => void;
canViewDeployment: boolean;
canViewOrganizations: boolean;
- canViewAllUsers: boolean;
canViewAuditLog: boolean;
canViewHealth: boolean;
proxyContextValue?: ProxyContextValue;
}
-export const Language = {
- workspaces: "Workspaces",
- templates: "Templates",
- users: "Users",
- audit: "Audit Logs",
- deployment: "Deployment",
+const linkStyles = {
+ default:
+ "text-sm font-medium text-content-secondary no-underline block h-full px-2 flex items-center hover:text-content-primary transition-colors",
+ active: "text-content-primary",
+};
+
+export const NavbarView: FC = ({
+ user,
+ logo_url,
+ docsHref,
+ buildInfo,
+ supportLinks,
+ onSignOut,
+ canViewDeployment,
+ canViewOrganizations,
+ canViewHealth,
+ canViewAuditLog,
+ proxyContextValue,
+}) => {
+ return (
+
+
+ {logo_url ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {proxyContextValue && (
+
+ )}
+
+
+
+
+ Docs
+
+
+ {user && (
+
+ )}
+
+
+
+
+ );
};
interface NavItemsProps {
@@ -41,199 +107,28 @@ interface NavItemsProps {
const NavItems: FC = ({ className }) => {
const location = useLocation();
- const theme = useTheme();
return (
-