mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat: Add preferences pages (#893)
This commit is contained in:
@ -1,12 +1,16 @@
|
||||
import React from "react"
|
||||
import { Route, Routes } from "react-router-dom"
|
||||
import { Navigate, Route, Routes } from "react-router-dom"
|
||||
import { AuthAndNav, RequireAuth } from "./components"
|
||||
import { PreferencesLayout } from "./components/Preferences/Layout"
|
||||
import { IndexPage } from "./pages"
|
||||
import { NotFoundPage } from "./pages/404"
|
||||
import { CliAuthenticationPage } from "./pages/cli-auth"
|
||||
import { HealthzPage } from "./pages/healthz"
|
||||
import { SignInPage } from "./pages/login"
|
||||
import { PreferencesPage } from "./pages/preferences"
|
||||
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 { TemplatesPage } from "./pages/templates"
|
||||
import { TemplatePage } from "./pages/templates/[organization]/[template]"
|
||||
import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create"
|
||||
@ -68,15 +72,12 @@ export const AppRouter: React.FC = () => (
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="preferences">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndNav>
|
||||
<PreferencesPage />
|
||||
</AuthAndNav>
|
||||
}
|
||||
/>
|
||||
<Route path="preferences" element={<PreferencesLayout />}>
|
||||
<Route index element={<Navigate to="account" />} />
|
||||
<Route path="account" element={<PreferencesAccountPage />} />
|
||||
<Route path="security" element={<PreferencesSecurityPage />} />
|
||||
<Route path="ssh-keys" element={<PreferencesSSHKeysPage />} />
|
||||
<Route path="linked-accounts" element={<PreferencesLinkedAccountsPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Using path="*"" means "match anything", so this route
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { Panel, PanelProps } from "./"
|
||||
|
||||
export default {
|
||||
title: "Page/Panel",
|
||||
component: Panel,
|
||||
}
|
||||
|
||||
const Template: Story<PanelProps> = (args: PanelProps) => <Panel {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
title: "Panel title",
|
||||
activeTab: "oauthSettings",
|
||||
menuItems: [
|
||||
{ label: "OAuth Settings", value: "oauthSettings" },
|
||||
{ label: "Security", value: "oauthSettings", hasChanges: true },
|
||||
{ label: "Hardware", value: "oauthSettings" },
|
||||
],
|
||||
}
|
34
site/src/components/Preferences/Layout.tsx
Normal file
34
site/src/components/Preferences/Layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import Box from "@material-ui/core/Box"
|
||||
import React from "react"
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { AuthAndNav } from "../Page"
|
||||
import { TabPanel } from "../TabPanel"
|
||||
|
||||
export const Language = {
|
||||
accountLabel: "Account",
|
||||
securityLabel: "Security",
|
||||
sshKeysLabel: "SSH Keys",
|
||||
linkedAccountsLabel: "Linked Accounts",
|
||||
preferencesLabel: "Preferences",
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ label: Language.accountLabel, path: "/preferences/account" },
|
||||
{ label: Language.securityLabel, path: "/preferences/security" },
|
||||
{ label: Language.sshKeysLabel, path: "/preferences/ssh-keys" },
|
||||
{ label: Language.linkedAccountsLabel, path: "/preferences/linked-accounts" },
|
||||
]
|
||||
|
||||
export const PreferencesLayout: React.FC = () => {
|
||||
return (
|
||||
<AuthAndNav>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box style={{ maxWidth: "1380px", margin: "1em auto" }}>
|
||||
<TabPanel title={Language.preferencesLabel} menuItems={menuItems}>
|
||||
<Outlet />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Box>
|
||||
</AuthAndNav>
|
||||
)
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { Sidebar, SidebarProps } from "./"
|
||||
|
||||
export default {
|
||||
title: "Page/Sidebar",
|
||||
component: Sidebar,
|
||||
}
|
||||
|
||||
const Template: Story<SidebarProps> = (args: SidebarProps) => <Sidebar {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
activeItem: "oauthSettings",
|
||||
menuItems: [
|
||||
{ label: "OAuth Settings", value: "oauthSettings" },
|
||||
{ label: "Security", value: "security", hasChanges: true },
|
||||
{ label: "Hardware", value: "hardware" },
|
||||
],
|
||||
}
|
20
site/src/components/TabPanel/TabPanel.stories.tsx
Normal file
20
site/src/components/TabPanel/TabPanel.stories.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { TabPanel, TabPanelProps } from "."
|
||||
|
||||
export default {
|
||||
title: "TabPanel/TabPanel",
|
||||
component: TabPanel,
|
||||
}
|
||||
|
||||
const Template: Story<TabPanelProps> = (args: TabPanelProps) => <TabPanel {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
title: "Title",
|
||||
menuItems: [
|
||||
{ label: "OAuth Settings", path: "oauthSettings" },
|
||||
{ label: "Security", path: "oauthSettings", hasChanges: true },
|
||||
{ label: "Hardware", path: "oauthSettings" },
|
||||
],
|
||||
}
|
19
site/src/components/TabPanel/TabSidebar.stories.tsx
Normal file
19
site/src/components/TabPanel/TabSidebar.stories.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { TabSidebar, TabSidebarProps } from "./TabSidebar"
|
||||
|
||||
export default {
|
||||
title: "TabPanel/TabSidebar",
|
||||
component: TabSidebar,
|
||||
}
|
||||
|
||||
const Template: Story<TabSidebarProps> = (args: TabSidebarProps) => <TabSidebar {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
menuItems: [
|
||||
{ label: "OAuth Settings", path: "oauthSettings" },
|
||||
{ label: "Security", path: "security", hasChanges: true },
|
||||
{ label: "Hardware", path: "hardware" },
|
||||
],
|
||||
}
|
@ -2,41 +2,35 @@ import List from "@material-ui/core/List"
|
||||
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"
|
||||
|
||||
export interface SidebarItem {
|
||||
value: string
|
||||
export interface TabSidebarItem {
|
||||
path: string
|
||||
label: string
|
||||
hasChanges?: boolean
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
menuItems: SidebarItem[]
|
||||
activeItem?: string
|
||||
onSelect?: (value: string) => void
|
||||
export interface TabSidebarProps {
|
||||
menuItems: TabSidebarItem[]
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ menuItems, activeItem, onSelect }) => {
|
||||
export const TabSidebar: React.FC<TabSidebarProps> = ({ menuItems }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<List className={styles.menu}>
|
||||
{menuItems.map(({ hasChanges, ...tab }) => {
|
||||
const isActive = activeItem === tab.value
|
||||
return (
|
||||
<ListItem
|
||||
key={tab.value}
|
||||
button
|
||||
onClick={onSelect ? () => onSelect(tab.value) : undefined}
|
||||
className={styles.menuItem}
|
||||
disableRipple
|
||||
focusRipple={false}
|
||||
component="li"
|
||||
>
|
||||
<span className={combineClasses({ [styles.menuItemSpan]: true, active: isActive })}>
|
||||
{hasChanges ? `${tab.label}*` : tab.label}
|
||||
</span>
|
||||
</ListItem>
|
||||
<NavLink to={tab.path} key={tab.path} className={styles.link}>
|
||||
{({ isActive }) => (
|
||||
<ListItem button className={styles.menuItem} disableRipple focusRipple={false} component="li">
|
||||
<span className={combineClasses({ [styles.menuItemSpan]: true, active: isActive })}>
|
||||
{hasChanges ? `${tab.label}*` : tab.label}
|
||||
</span>
|
||||
</ListItem>
|
||||
)}
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
@ -49,6 +43,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
|
||||
link: {
|
||||
textDecoration: "none",
|
||||
},
|
||||
|
||||
menuItem: {
|
||||
letterSpacing: -theme.spacing(0.0375),
|
||||
padding: 0,
|
@ -1,18 +1,16 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator"
|
||||
import React from "react"
|
||||
import { Sidebar, SidebarItem } from "../Sidebar"
|
||||
import { TabSidebar, TabSidebarItem } from "./TabSidebar"
|
||||
|
||||
export type AdminMenuItemCallback = (menuItem: string) => void
|
||||
|
||||
export interface PanelProps {
|
||||
export interface TabPanelProps {
|
||||
title: string
|
||||
menuItems: SidebarItem[]
|
||||
activeTab: string
|
||||
onSelect: AdminMenuItemCallback
|
||||
menuItems: TabSidebarItem[]
|
||||
}
|
||||
|
||||
export const Panel: React.FC<PanelProps> = ({ children, title, menuItems, activeTab, onSelect }) => {
|
||||
export const TabPanel: React.FC<TabPanelProps> = ({ children, title, menuItems }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
@ -20,7 +18,7 @@ export const Panel: React.FC<PanelProps> = ({ children, title, menuItems, active
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.menuPanel}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<Sidebar menuItems={menuItems} activeItem={activeTab} onSelect={onSelect} />
|
||||
<TabSidebar menuItems={menuItems} />
|
||||
</div>
|
||||
|
||||
<div className={styles.contentPanel}>{children}</div>
|
11
site/src/pages/preferences/account.tsx
Normal file
11
site/src/pages/preferences/account.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react"
|
||||
import { Section } from "../../components/Section"
|
||||
|
||||
const Language = {
|
||||
title: "Account",
|
||||
description: "Update your display name, email, profile picture, and dotfiles preferences.",
|
||||
}
|
||||
|
||||
export const PreferencesAccountPage: React.FC = () => {
|
||||
return <Section title={Language.title} description={Language.description} />
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import Box from "@material-ui/core/Box"
|
||||
import Paper from "@material-ui/core/Paper"
|
||||
import React from "react"
|
||||
import { Header } from "../../components/Header"
|
||||
import { Footer } from "../../components/Page"
|
||||
|
||||
export const PreferencesPage: React.FC = () => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Header title="Preferences" />
|
||||
<Paper style={{ maxWidth: "1380px", margin: "1em auto", width: "100%" }}>Preferences here!</Paper>
|
||||
<Footer />
|
||||
</Box>
|
||||
)
|
||||
}
|
12
site/src/pages/preferences/linked-accounts.tsx
Normal file
12
site/src/pages/preferences/linked-accounts.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react"
|
||||
import { Section } from "../../components/Section"
|
||||
|
||||
const Language = {
|
||||
title: "Linked Accounts",
|
||||
description:
|
||||
"Linking your Coder account will add your workspace SSH key, allowing you to perform Git actions on all your workspaces.",
|
||||
}
|
||||
|
||||
export const PreferencesLinkedAccountsPage: React.FC = () => {
|
||||
return <Section title={Language.title} description={Language.description} />
|
||||
}
|
11
site/src/pages/preferences/security.tsx
Normal file
11
site/src/pages/preferences/security.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react"
|
||||
import { Section } from "../../components/Section"
|
||||
|
||||
const Language = {
|
||||
title: "Security",
|
||||
description: "Changing your password will sign you out of your current session.",
|
||||
}
|
||||
|
||||
export const PreferencesSecurityPage: React.FC = () => {
|
||||
return <Section title={Language.title} description={Language.description} />
|
||||
}
|
12
site/src/pages/preferences/ssh-keys.tsx
Normal file
12
site/src/pages/preferences/ssh-keys.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react"
|
||||
import { Section } from "../../components/Section"
|
||||
|
||||
const Language = {
|
||||
title: "SSH Keys",
|
||||
description:
|
||||
"Coder automatically inserts a private key into every workspace; you can add the corresponding public key to any services (such as Git) that you need access to from your workspace.",
|
||||
}
|
||||
|
||||
export const PreferencesSSHKeysPage: React.FC = () => {
|
||||
return <Section title={Language.title} description={Language.description} />
|
||||
}
|
Reference in New Issue
Block a user