diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 51a6895715f2d..b15e06fb710e0 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -7,13 +7,16 @@ import { NotFoundPage } from "./pages/404" import { CliAuthenticationPage } from "./pages/cli-auth" import { HealthzPage } from "./pages/healthz" import { SignInPage } from "./pages/login" +import { OrganizationsPage } from "./pages/orgs" import { PreferencesAccountPage } from "./pages/preferences/account" import { PreferencesLinkedAccountsPage } from "./pages/preferences/linked-accounts" import { PreferencesSecurityPage } from "./pages/preferences/security" import { PreferencesSSHKeysPage } from "./pages/preferences/ssh-keys" +import { SettingsPage } from "./pages/settings" import { TemplatesPage } from "./pages/templates" import { TemplatePage } from "./pages/templates/[organization]/[template]" import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create" +import { UsersPage } from "./pages/users" import { WorkspacePage } from "./pages/workspaces/[workspace]" export const AppRouter: React.FC = () => ( @@ -72,6 +75,31 @@ export const AppRouter: React.FC = () => ( /> + + + + } + /> + + + + } + /> + + + + } + /> + }> } /> } /> diff --git a/site/src/components/AdminDropdown/AdminDropdown.stories.tsx b/site/src/components/AdminDropdown/AdminDropdown.stories.tsx new file mode 100644 index 0000000000000..8cc6c7d70ce15 --- /dev/null +++ b/site/src/components/AdminDropdown/AdminDropdown.stories.tsx @@ -0,0 +1,17 @@ +import Box from "@material-ui/core/Box" +import { Story } from "@storybook/react" +import React from "react" +import { AdminDropdown } from "./AdminDropdown" + +export default { + title: "components/AdminDropdown", + component: AdminDropdown, +} + +const Template: Story = () => ( + + + +) + +export const Example = Template.bind({}) diff --git a/site/src/components/AdminDropdown/AdminDropdown.test.tsx b/site/src/components/AdminDropdown/AdminDropdown.test.tsx new file mode 100644 index 0000000000000..cf74eb782f7da --- /dev/null +++ b/site/src/components/AdminDropdown/AdminDropdown.test.tsx @@ -0,0 +1,48 @@ +import { screen } from "@testing-library/react" +import React from "react" +import { history, render } from "../../test_helpers" +import { AdminDropdown, Language } from "./AdminDropdown" + +const renderAndClick = async () => { + render() + const trigger = await screen.findByText(Language.menuTitle) + trigger.click() +} + +describe("AdminDropdown", () => { + describe("when the trigger is clicked", () => { + it("opens the menu", async () => { + await renderAndClick() + expect(screen.getByText(Language.usersLabel)).toBeDefined() + expect(screen.getByText(Language.orgsLabel)).toBeDefined() + expect(screen.getByText(Language.settingsLabel)).toBeDefined() + }) + }) + + it("links to the users page", async () => { + await renderAndClick() + + const usersLink = screen.getByText(Language.usersLabel).closest("a") + usersLink?.click() + + expect(history.location.pathname).toEqual("/users") + }) + + it("links to the orgs page", async () => { + await renderAndClick() + + const usersLink = screen.getByText(Language.orgsLabel).closest("a") + usersLink?.click() + + expect(history.location.pathname).toEqual("/orgs") + }) + + it("links to the settings page", async () => { + await renderAndClick() + + const usersLink = screen.getByText(Language.settingsLabel).closest("a") + usersLink?.click() + + expect(history.location.pathname).toEqual("/settings") + }) +}) diff --git a/site/src/components/AdminDropdown/AdminDropdown.tsx b/site/src/components/AdminDropdown/AdminDropdown.tsx new file mode 100644 index 0000000000000..455331e66d18f --- /dev/null +++ b/site/src/components/AdminDropdown/AdminDropdown.tsx @@ -0,0 +1,155 @@ +import ListItem from "@material-ui/core/ListItem" +import ListItemText from "@material-ui/core/ListItemText" +import { fade, makeStyles, Theme } from "@material-ui/core/styles" +import AdminIcon from "@material-ui/icons/SettingsOutlined" +import React, { useState } from "react" +import { navHeight } from "../../theme/constants" +import { BorderedMenu } from "../BorderedMenu/BorderedMenu" +import { BorderedMenuRow } from "../BorderedMenuRow/BorderedMenuRow" +import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows" +import { BuildingIcon } from "../Icons/BuildingIcon" +import { UsersOutlinedIcon } from "../Icons/UsersOutlinedIcon" + +export const Language = { + menuTitle: "Admin", + usersLabel: "Users", + usersDescription: "Manage users, roles, and permissions.", + orgsLabel: "Organizations", + orgsDescription: "Manage organizations.", + settingsLabel: "Settings", + settingsDescription: "Configure authentication and more.", +} + +const entries = [ + { + label: Language.usersLabel, + description: Language.usersDescription, + path: "/users", + Icon: UsersOutlinedIcon, + }, + { + label: Language.orgsLabel, + description: Language.orgsDescription, + path: "/orgs", + Icon: BuildingIcon, + }, + { + label: Language.settingsLabel, + description: Language.settingsDescription, + path: "/settings", + Icon: AdminIcon, + }, +] + +export const AdminDropdown: React.FC = () => { + const styles = useStyles() + const [anchorEl, setAnchorEl] = useState() + const onClose = () => setAnchorEl(undefined) + const onOpenAdminMenu = (ev: React.MouseEvent) => setAnchorEl(ev.currentTarget) + + return ( + <> +
+ + + {anchorEl ? : } + +
+ + + {entries.map((entry) => ( + { + onClose() + }} + /> + ))} + + + ) +} + +const useStyles = makeStyles((theme: Theme) => ({ + link: { + "&:focus": { + outline: "none", + + "& .MuiListItem-button": { + background: fade(theme.palette.primary.light, 0.1), + }, + }, + + "& .MuiListItemText-root": { + display: "flex", + flexDirection: "column", + alignItems: "center", + }, + "& .feature-stage-chip": { + position: "absolute", + bottom: theme.spacing(1), + + "& .MuiChip-labelSmall": { + fontSize: "10px", + }, + }, + whiteSpace: "nowrap", + "& .MuiListItem-button": { + height: navHeight, + color: "#A7A7A7", + padding: `0 ${theme.spacing(3)}px`, + + "&.Mui-selected": { + background: "transparent", + "& .MuiListItemText-root": { + color: theme.palette.primary.contrastText, + + "&:not(.no-brace) .MuiTypography-root": { + position: "relative", + + "&::before": { + content: `"{"`, + left: -14, + position: "absolute", + }, + "&::after": { + content: `"}"`, + position: "absolute", + right: -14, + }, + }, + }, + }, + + "&.Mui-focusVisible, &:hover": { + background: "#333", + }, + + "& .MuiListItemText-primary": { + fontFamily: theme.typography.fontFamily, + fontSize: 16, + fontWeight: 500, + }, + }, + }, +})) diff --git a/site/src/components/BorderedMenu/BorderedMenu.stories.tsx b/site/src/components/BorderedMenu/BorderedMenu.stories.tsx new file mode 100644 index 0000000000000..73186fd77faec --- /dev/null +++ b/site/src/components/BorderedMenu/BorderedMenu.stories.tsx @@ -0,0 +1,30 @@ +import { Story } from "@storybook/react" +import React from "react" +import { BorderedMenuRow } from "../BorderedMenuRow/BorderedMenuRow" +import { BuildingIcon } from "../Icons/BuildingIcon" +import { UsersOutlinedIcon } from "../Icons/UsersOutlinedIcon" +import { BorderedMenu, BorderedMenuProps } from "./BorderedMenu" + +export default { + title: "components/BorderedMenu", + component: BorderedMenu, +} + +const Template: Story = (args: BorderedMenuProps) => ( + + + + +) + +export const AdminVariant = Template.bind({}) +AdminVariant.args = { + variant: "admin-dropdown", + open: true, +} + +export const UserVariant = Template.bind({}) +UserVariant.args = { + variant: "user-dropdown", + open: true, +} diff --git a/site/src/components/Navbar/BorderedMenu.tsx b/site/src/components/BorderedMenu/BorderedMenu.tsx similarity index 67% rename from site/src/components/Navbar/BorderedMenu.tsx rename to site/src/components/BorderedMenu/BorderedMenu.tsx index 1228d530673a7..3842b4be785a9 100644 --- a/site/src/components/Navbar/BorderedMenu.tsx +++ b/site/src/components/BorderedMenu/BorderedMenu.tsx @@ -2,9 +2,9 @@ import Popover, { PopoverProps } from "@material-ui/core/Popover" import { fade, makeStyles } from "@material-ui/core/styles" import React from "react" -type BorderedMenuVariant = "manage-dropdown" | "user-dropdown" +type BorderedMenuVariant = "admin-dropdown" | "user-dropdown" -type BorderedMenuProps = Omit & { +export type BorderedMenuProps = Omit & { variant?: BorderedMenuVariant } @@ -20,7 +20,14 @@ export const BorderedMenu: React.FC = ({ children, variant, . const useStyles = makeStyles((theme) => ({ root: { - paddingBottom: theme.spacing(1), + "&[data-variant='admin-dropdown'] $paperRoot": { + padding: `${theme.spacing(3)}px 0`, + }, + + "&[data-variant='user-dropdown'] $paperRoot": { + paddingBottom: theme.spacing(1), + width: 292, + }, }, paperRoot: { width: "292px", diff --git a/site/src/components/BorderedMenuRow/BorderedMenuRow.tsx b/site/src/components/BorderedMenuRow/BorderedMenuRow.tsx new file mode 100644 index 0000000000000..d83bebdf52166 --- /dev/null +++ b/site/src/components/BorderedMenuRow/BorderedMenuRow.tsx @@ -0,0 +1,144 @@ +import ListItem from "@material-ui/core/ListItem" +import { makeStyles } from "@material-ui/core/styles" +import SvgIcon from "@material-ui/core/SvgIcon" +import CheckIcon from "@material-ui/icons/Check" +import React from "react" +import { NavLink } from "react-router-dom" +import { ellipsizeText } from "../../util/ellipsizeText" +import { Typography } from "../Typography/Typography" + +type BorderedMenuRowVariant = "narrow" | "wide" + +interface BorderedMenuRowProps { + /** `true` indicates this row is currently selected */ + active?: boolean + /** Optional description that appears beneath the title */ + description?: string + /** An SvgIcon that will be rendered to the left of the title */ + Icon: typeof SvgIcon + /** URL path */ + path?: string + /** Required title of this row */ + title: string + /** Defaults to `"wide"` */ + variant?: BorderedMenuRowVariant + /** Callback fired when this row is clicked */ + onClick?: () => void +} + +export const BorderedMenuRow: React.FC = ({ + active, + description, + Icon, + path, + title, + variant, + onClick, +}) => { + const styles = useStyles() + + const Component = () => ( + +
+
+ + {title} + {active && } +
+ + {description && ( + + {ellipsizeText(description)} + + )} +
+
+ ) + + if (path) { + return ( + + + + ) + } + + return +} + +const iconSize = 20 + +const useStyles = makeStyles((theme) => ({ + root: { + cursor: "pointer", + padding: `0 ${theme.spacing(1)}px`, + + "&:hover": { + backgroundColor: "unset", + "& $content": { + backgroundColor: theme.palette.background.default, + }, + }, + + "&[data-status='active']": { + color: theme.palette.primary.main, + "& .BorderedMenuRow-description": { + color: theme.palette.text.primary, + }, + "& .BorderedMenuRow-icon": { + color: theme.palette.primary.main, + }, + }, + }, + rootGutters: { + padding: `0 ${theme.spacing(1.5)}px`, + }, + content: { + borderRadius: 7, + display: "flex", + flexDirection: "column", + padding: theme.spacing(2), + width: 320, + + "&[data-variant='narrow']": { + width: 268, + }, + }, + contentTop: { + alignItems: "center", + display: "flex", + }, + icon: { + color: theme.palette.text.secondary, + height: iconSize, + width: iconSize, + + "& path": { + fill: theme.palette.text.secondary, + }, + }, + link: { + textDecoration: "none", + color: "inherit", + }, + title: { + fontSize: 16, + fontWeight: 500, + lineHeight: 1.5, + marginLeft: theme.spacing(2), + }, + checkMark: { + height: iconSize, + marginLeft: "auto", + width: iconSize, + }, + description: { + marginLeft: theme.spacing(4.5), + marginTop: theme.spacing(0.5), + }, +})) diff --git a/site/src/components/DropdownArrows/DropdownArrows.tsx b/site/src/components/DropdownArrows/DropdownArrows.tsx new file mode 100644 index 0000000000000..4e236903f109e --- /dev/null +++ b/site/src/components/DropdownArrows/DropdownArrows.tsx @@ -0,0 +1,26 @@ +import { fade, makeStyles, Theme } from "@material-ui/core/styles" +import KeyboardArrowDown from "@material-ui/icons/KeyboardArrowDown" +import KeyboardArrowUp from "@material-ui/icons/KeyboardArrowUp" +import React from "react" + +const useStyles = makeStyles((theme: Theme) => ({ + arrowIcon: { + color: fade(theme.palette.primary.contrastText, 0.7), + marginLeft: theme.spacing(1), + width: 16, + height: 16, + }, + arrowIconUp: { + color: theme.palette.primary.contrastText, + }, +})) + +export const OpenDropdown: React.FC = () => { + const styles = useStyles() + return +} + +export const CloseDropdown: React.FC = () => { + const styles = useStyles() + return +} diff --git a/site/src/components/Icons/BuildingIcon.tsx b/site/src/components/Icons/BuildingIcon.tsx new file mode 100644 index 0000000000000..381cd106154e4 --- /dev/null +++ b/site/src/components/Icons/BuildingIcon.tsx @@ -0,0 +1,13 @@ +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon" +import React from "react" + +export const BuildingIcon = (props: SvgIconProps): JSX.Element => ( + + + +) diff --git a/site/src/components/Icons/UsersOutlinedIcon.tsx b/site/src/components/Icons/UsersOutlinedIcon.tsx new file mode 100644 index 0000000000000..b297380d125e4 --- /dev/null +++ b/site/src/components/Icons/UsersOutlinedIcon.tsx @@ -0,0 +1,25 @@ +import SvgIcon from "@material-ui/core/SvgIcon" +import React from "react" + +export const UsersOutlinedIcon: typeof SvgIcon = (props) => ( + + + + + + + + +) diff --git a/site/src/components/Navbar/NavbarView.stories.tsx b/site/src/components/Navbar/NavbarView/NavbarView.stories.tsx similarity index 53% rename from site/src/components/Navbar/NavbarView.stories.tsx rename to site/src/components/Navbar/NavbarView/NavbarView.stories.tsx index d6e9e5d30493b..e5d43d916d09d 100644 --- a/site/src/components/Navbar/NavbarView.stories.tsx +++ b/site/src/components/Navbar/NavbarView/NavbarView.stories.tsx @@ -1,9 +1,9 @@ import { Story } from "@storybook/react" import React from "react" -import { NavbarView, NavbarViewProps } from "./NavbarView" +import { NavbarView, NavbarViewProps } from "." export default { - title: "Page/NavbarView", + title: "components/NavbarView", component: NavbarView, argTypes: { onSignOut: { action: "Sign Out" }, @@ -12,8 +12,16 @@ export default { const Template: Story = (args: NavbarViewProps) => -export const Primary = Template.bind({}) -Primary.args = { +export const ForAdmin = Template.bind({}) +ForAdmin.args = { + user: { id: "1", username: "Administrator", email: "admin@coder.com", created_at: "dawn" }, + onSignOut: () => { + return Promise.resolve() + }, +} + +export const ForMember = Template.bind({}) +ForMember.args = { user: { id: "1", username: "CathyCoder", email: "cathy@coder.com", created_at: "dawn" }, onSignOut: () => { return Promise.resolve() diff --git a/site/src/components/Navbar/NavbarView.test.tsx b/site/src/components/Navbar/NavbarView/NavbarView.test.tsx similarity index 82% rename from site/src/components/Navbar/NavbarView.test.tsx rename to site/src/components/Navbar/NavbarView/NavbarView.test.tsx index 8fd54020ccc13..3a2f9174a8237 100644 --- a/site/src/components/Navbar/NavbarView.test.tsx +++ b/site/src/components/Navbar/NavbarView/NavbarView.test.tsx @@ -1,8 +1,8 @@ import { screen } from "@testing-library/react" import React from "react" -import { render } from "../../test_helpers" -import { MockUser } from "../../test_helpers/entities" -import { NavbarView } from "./NavbarView" +import { NavbarView } from "." +import { render } from "../../../test_helpers" +import { MockUser } from "../../../test_helpers/entities" describe("NavbarView", () => { const noop = () => { diff --git a/site/src/components/Navbar/NavbarView.tsx b/site/src/components/Navbar/NavbarView/index.tsx similarity index 86% rename from site/src/components/Navbar/NavbarView.tsx rename to site/src/components/Navbar/NavbarView/index.tsx index 728179c529ea0..58a9b387f9285 100644 --- a/site/src/components/Navbar/NavbarView.tsx +++ b/site/src/components/Navbar/NavbarView/index.tsx @@ -3,9 +3,11 @@ import ListItem from "@material-ui/core/ListItem" import { fade, makeStyles } from "@material-ui/core/styles" import React from "react" import { NavLink } from "react-router-dom" -import { UserResponse } from "../../api/types" -import { Logo } from "../Icons" -import { UserDropdown } from "./UserDropdown" +import { UserResponse } from "../../../api/types" +import { navHeight } from "../../../theme/constants" +import { AdminDropdown } from "../../AdminDropdown/AdminDropdown" +import { Logo } from "../../Icons" +import { UserDropdown } from "../UserDropdown" export interface NavbarViewProps { user?: UserResponse @@ -29,6 +31,7 @@ export const NavbarView: React.FC = ({ user, onSignOut }) => {
+ {user && user.email === "admin@coder.com" && }
{user && }
) @@ -42,7 +45,7 @@ const useStyles = makeStyles((theme) => ({ flexDirection: "row", justifyContent: "center", alignItems: "center", - height: 56, + height: navHeight, background: theme.palette.navbar.main, marginTop: 0, transition: "margin 150ms ease", @@ -62,7 +65,7 @@ const useStyles = makeStyles((theme) => ({ logo: { alignItems: "center", display: "flex", - height: 56, + height: navHeight, paddingLeft: theme.spacing(4), paddingRight: theme.spacing(2), "& svg": { @@ -81,7 +84,7 @@ const useStyles = makeStyles((theme) => ({ color: "#A7A7A7", display: "flex", fontSize: 16, - height: 56, + height: navHeight, padding: `0 ${theme.spacing(3)}px`, textDecoration: "none", transition: "background-color 0.3s ease", diff --git a/site/src/components/Navbar/UserDropdown.stories.tsx b/site/src/components/Navbar/UserDropdown/UserDropdown.stories.tsx similarity index 90% rename from site/src/components/Navbar/UserDropdown.stories.tsx rename to site/src/components/Navbar/UserDropdown/UserDropdown.stories.tsx index fb83f3be70f54..f6ed3bccdae59 100644 --- a/site/src/components/Navbar/UserDropdown.stories.tsx +++ b/site/src/components/Navbar/UserDropdown/UserDropdown.stories.tsx @@ -1,7 +1,7 @@ import Box from "@material-ui/core/Box" import { Story } from "@storybook/react" import React from "react" -import { UserDropdown, UserDropdownProps } from "./UserDropdown" +import { UserDropdown, UserDropdownProps } from "." export default { title: "Page/UserDropdown", diff --git a/site/src/components/Navbar/UserDropdown.test.tsx b/site/src/components/Navbar/UserDropdown/UserDropdown.test.tsx similarity index 90% rename from site/src/components/Navbar/UserDropdown.test.tsx rename to site/src/components/Navbar/UserDropdown/UserDropdown.test.tsx index 44730fd31f56f..fc4c106deb10b 100644 --- a/site/src/components/Navbar/UserDropdown.test.tsx +++ b/site/src/components/Navbar/UserDropdown/UserDropdown.test.tsx @@ -1,8 +1,8 @@ import { screen } from "@testing-library/react" import React from "react" -import { render } from "../../test_helpers" -import { MockUser } from "../../test_helpers/entities" -import { Language, UserDropdown, UserDropdownProps } from "./UserDropdown" +import { Language, UserDropdown, UserDropdownProps } from "." +import { render } from "../../../test_helpers" +import { MockUser } from "../../../test_helpers/entities" const renderAndClick = async (props: Partial = {}) => { render() diff --git a/site/src/components/Navbar/UserDropdown.tsx b/site/src/components/Navbar/UserDropdown/index.tsx similarity index 81% rename from site/src/components/Navbar/UserDropdown.tsx rename to site/src/components/Navbar/UserDropdown/index.tsx index 4b8bce5b2d64d..e220bd12da2a7 100644 --- a/site/src/components/Navbar/UserDropdown.tsx +++ b/site/src/components/Navbar/UserDropdown/index.tsx @@ -5,16 +5,15 @@ import ListItemText from "@material-ui/core/ListItemText" import MenuItem from "@material-ui/core/MenuItem" import { fade, makeStyles } from "@material-ui/core/styles" import AccountIcon from "@material-ui/icons/AccountCircleOutlined" -import KeyboardArrowDown from "@material-ui/icons/KeyboardArrowDown" -import KeyboardArrowUp from "@material-ui/icons/KeyboardArrowUp" import React, { useState } from "react" import { Link } from "react-router-dom" -import { UserResponse } from "../../api/types" -import { LogoutIcon } from "../Icons" -import { DocsIcon } from "../Icons/DocsIcon" -import { UserAvatar } from "../User" -import { UserProfileCard } from "../User/UserProfileCard" -import { BorderedMenu } from "./BorderedMenu" +import { UserResponse } from "../../../api/types" +import { BorderedMenu } from "../../BorderedMenu/BorderedMenu" +import { CloseDropdown, OpenDropdown } from "../../DropdownArrows/DropdownArrows" +import { LogoutIcon } from "../../Icons" +import { DocsIcon } from "../../Icons/DocsIcon" +import { UserAvatar } from "../../User" +import { UserProfileCard } from "../../User/UserProfileCard" export const Language = { accountLabel: "Account", @@ -44,11 +43,7 @@ export const UserDropdown: React.FC = ({ user, onSignOut }: U - {anchorEl ? ( - - ) : ( - - )} + {anchorEl ? : }
@@ -120,17 +115,6 @@ export const useStyles = makeStyles((theme) => ({ marginBottom: theme.spacing(1), }, - arrowIcon: { - color: fade(theme.palette.primary.contrastText, 0.7), - marginLeft: theme.spacing(1), - width: 16, - height: 16, - }, - - arrowIconUp: { - color: theme.palette.primary.contrastText, - }, - menuItem: { height: 44, padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`, diff --git a/site/src/components/TabPanel/TabSidebar.tsx b/site/src/components/TabPanel/TabSidebar.tsx index 598aa69329c98..99973084d670c 100644 --- a/site/src/components/TabPanel/TabSidebar.tsx +++ b/site/src/components/TabPanel/TabSidebar.tsx @@ -3,7 +3,7 @@ import ListItem from "@material-ui/core/ListItem" import { makeStyles } from "@material-ui/core/styles" import React from "react" import { NavLink } from "react-router-dom" -import { combineClasses } from "../../util/combine-classes" +import { combineClasses } from "../../util/combineClasses" export interface TabSidebarItem { path: string diff --git a/site/src/components/Typography/Typography.stories.tsx b/site/src/components/Typography/Typography.stories.tsx new file mode 100644 index 0000000000000..87fc049446d2b --- /dev/null +++ b/site/src/components/Typography/Typography.stories.tsx @@ -0,0 +1,24 @@ +import { Story } from "@storybook/react" +import React from "react" +import { Typography, TypographyProps } from "./Typography" + +export default { + title: "components/Typography", + component: Typography, +} + +const Template: Story = (args: TypographyProps) => ( + <> + Colorless green ideas sleep furiously + More people have been to France than I have + +) + +export const Short = Template.bind({}) +Short.args = { + short: true, +} +export const Tall = Template.bind({}) +Tall.args = { + short: false, +} diff --git a/site/src/components/Typography/Typography.tsx b/site/src/components/Typography/Typography.tsx new file mode 100644 index 0000000000000..ef5210dfa6ffb --- /dev/null +++ b/site/src/components/Typography/Typography.tsx @@ -0,0 +1,42 @@ +/** + * @fileoverview (TODO: Grey) This file is in a temporary state and is a + * verbatim port from `@coder/ui`. + */ + +import { makeStyles } from "@material-ui/core/styles" +import MuiTypography, { TypographyProps as MuiTypographyProps } from "@material-ui/core/Typography" +import * as React from "react" +import { appendCSSString, combineClasses } from "../../util/combineClasses" + +export interface TypographyProps extends MuiTypographyProps { + short?: boolean +} + +/** + * Wrapper around Material UI's Typography component to allow for future + * custom typography types. + * + * See original component's Material UI documentation here: https://material-ui.com/components/typography/ + */ +export const Typography: React.FC = ({ className, short, ...rest }) => { + const styles = useStyles() + + let classes = combineClasses({ [styles.short]: short }) + if (className) { + classes = appendCSSString(classes ?? "", className) + } + + return +} + +const useStyles = makeStyles({ + short: { + "&.MuiTypography-body1": { + lineHeight: "21px", + }, + "&.MuiTypography-body2": { + lineHeight: "18px", + letterSpacing: 0.2, + }, + }, +}) diff --git a/site/src/pages/orgs/index.tsx b/site/src/pages/orgs/index.tsx new file mode 100644 index 0000000000000..6278f8b31a255 --- /dev/null +++ b/site/src/pages/orgs/index.tsx @@ -0,0 +1,5 @@ +import React from "react" + +export const OrganizationsPage: React.FC = () => { + return
Coming soon!
+} diff --git a/site/src/pages/settings/index.tsx b/site/src/pages/settings/index.tsx new file mode 100644 index 0000000000000..fba4845cccae4 --- /dev/null +++ b/site/src/pages/settings/index.tsx @@ -0,0 +1,5 @@ +import React from "react" + +export const SettingsPage: React.FC = () => { + return
Coming soon!
+} diff --git a/site/src/pages/users.tsx b/site/src/pages/users.tsx new file mode 100644 index 0000000000000..a04e1ab5b4646 --- /dev/null +++ b/site/src/pages/users.tsx @@ -0,0 +1,5 @@ +import React from "react" + +export const UsersPage: React.FC = () => { + return
Coming soon!
+} diff --git a/site/src/theme/constants.ts b/site/src/theme/constants.ts index 6a4aa1bb4c036..008063489519d 100644 --- a/site/src/theme/constants.ts +++ b/site/src/theme/constants.ts @@ -6,3 +6,4 @@ export const MONOSPACE_FONT_FAMILY = export const BODY_FONT_FAMILY = `"Inter", sans-serif` export const lightButtonShadow = "0 2px 2px rgba(0, 23, 121, 0.08)" export const emptyBoxShadow = "none" +export const navHeight = 56 diff --git a/site/src/util/combineClasses.test.ts b/site/src/util/combineClasses.test.ts new file mode 100644 index 0000000000000..7890f00f1df9e --- /dev/null +++ b/site/src/util/combineClasses.test.ts @@ -0,0 +1,28 @@ +import { combineClasses } from "./combineClasses" + +const staticStyles = { + text: "MuiText", + success: "MuiText-Green", + warning: "MuiText-Red", +} + +describe("combineClasses", () => { + it.each([ + // Falsy + [undefined, undefined], + [{ [staticStyles.text]: false }, undefined], + [{ [staticStyles.text]: undefined }, undefined], + [[], undefined], + + // Truthy + [{ [staticStyles.text]: true }, "MuiText"], + [{ [staticStyles.text]: true, [staticStyles.warning]: true }, "MuiText MuiText-Red"], + [[staticStyles.text], "MuiText"], + + // Mixed + [{ [staticStyles.text]: true, [staticStyles.success]: false }, "MuiText"], + [[staticStyles.text, staticStyles.success], "MuiText MuiText-Green"], + ])(`classNames(%p) returns %p`, (staticClasses, result) => { + expect(combineClasses(staticClasses)).toBe(result) + }) +}) diff --git a/site/src/util/combine-classes.ts b/site/src/util/combineClasses.ts similarity index 100% rename from site/src/util/combine-classes.ts rename to site/src/util/combineClasses.ts diff --git a/site/src/util/ellipsizeText.test.ts b/site/src/util/ellipsizeText.test.ts new file mode 100644 index 0000000000000..7c30b0ef1cf3b --- /dev/null +++ b/site/src/util/ellipsizeText.test.ts @@ -0,0 +1,17 @@ +import { ellipsizeText } from "./ellipsizeText" +import { Nullable } from "./nullable" + +describe("ellipsizeText", () => { + it.each([ + [undefined, 10, undefined], + [null, 10, undefined], + ["", 10, ""], + ["Hello World", "Hello World".length, "Hello World"], + ["Hello World", "Hello...".length, "Hello..."], + ])( + `ellipsizeText(%p, %p) returns %p`, + (str: Nullable, maxLength: number | undefined, output: Nullable) => { + expect(ellipsizeText(str, maxLength)).toBe(output) + }, + ) +}) diff --git a/site/src/util/ellipsizeText.ts b/site/src/util/ellipsizeText.ts new file mode 100644 index 0000000000000..7509eff789c9e --- /dev/null +++ b/site/src/util/ellipsizeText.ts @@ -0,0 +1,9 @@ +import { Nullable } from "./nullable" + +/** Truncates and ellipsizes text if it's longer than maxLength */ +export const ellipsizeText = (text: Nullable, maxLength = 80): string | undefined => { + if (typeof text !== "string") { + return + } + return text.length <= maxLength ? text : `${text.substr(0, maxLength - 3)}...` +} diff --git a/site/src/util/nullable.ts b/site/src/util/nullable.ts new file mode 100644 index 0000000000000..dda3b129e80c6 --- /dev/null +++ b/site/src/util/nullable.ts @@ -0,0 +1,5 @@ +/** + * A Nullable may be its concrete type, `null` or `undefined` + * @remark Exact opposite of the native TS type NonNullable + */ +export type Nullable = null | undefined | T