Refactored layout. Added NavBar component.

This commit is contained in:
Mike Cao 2023-01-18 15:05:39 -08:00
parent fad38dc180
commit 1d9c3133f0
56 changed files with 601 additions and 429 deletions

View file

@ -1,7 +1,7 @@
import { useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import TimezoneSetting from './TimezoneSetting';
import DateRangeSetting from './DateRangeSetting';
import TimezoneSetting from '../pages/settings/profile/TimezoneSetting';
import DateRangeSetting from '../pages/settings/profile/DateRangeSetting';
import { Button, Icon } from 'react-basics';
import styles from './SettingsButton.module.css';
import Gear from 'assets/gear.svg';

View file

@ -5,7 +5,7 @@ import { useIntl, defineMessages } from 'react-intl';
import DatePickerForm from 'components/metrics/DatePickerForm';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date';
import Calendar from 'assets/calendar-alt.svg';
import Calendar from 'assets/calendar.svg';
const messages = defineMessages({
today: { id: 'label.today', defaultMessage: 'Today' },

View file

@ -13,12 +13,12 @@ const menuItems = [
},
{ label: <FormattedMessage id="label.realtime" defaultMessage="Realtime" />, value: '/realtime' },
{
label: <FormattedMessage id="label.settings" defaultMessage="SettingsLayout" />,
value: '/settings',
label: <FormattedMessage id="label.settings" defaultMessage="AppLayout" />,
value: '/buttons',
},
{
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
value: '/settings/profile',
value: '/buttons/profile',
},
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: '/logout' },
];

13
components/icons.js Normal file
View file

@ -0,0 +1,13 @@
import Calendar from 'assets/calendar.svg';
import Clock from 'assets/clock.svg';
import Dashboard from 'assets/dashboard.svg';
import Gear from 'assets/gear.svg';
import Globe from 'assets/globe.svg';
import Logo from 'assets/logo.svg';
import Moon from 'assets/moon.svg';
import Profile from 'assets/profile.svg';
import Sun from 'assets/sun.svg';
import User from 'assets/user.svg';
import Users from 'assets/users.svg';
export { Calendar, Clock, Dashboard, Gear, Globe, Logo, Moon, Profile, Sun, User, Users };

View file

@ -1,22 +1,25 @@
import { Container } from 'react-basics';
import Head from 'next/head';
import Header from 'components/layout/Header';
import Footer from 'components/layout/Footer';
import useLocale from 'hooks/useLocale';
import NavBar from 'components/layout/NavBar';
import useRequireLogin from 'hooks/useRequireLogin';
import styles from './AppLayout.module.css';
export default function AppLayout({ title, children }) {
useRequireLogin();
const { dir } = useLocale();
return (
<Container dir={dir} style={{ maxWidth: 1140 }}>
<div className={styles.layout}>
<Head>
<title>{title ? `${title} | umami` : 'umami'}</title>
</Head>
<Header />
<main>{children}</main>
<Footer />
</Container>
<div className={styles.nav}>
<NavBar />
</div>
<div className={styles.body}>
<Container>
<main>{children}</main>
</Container>
</div>
</div>
);
}

View file

@ -1,6 +1,16 @@
.layout {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: minmax(60px, 200px) 1fr;
height: 100vh;
overflow: hidden;
}
.nav {
grid-row: 1 / 3;
}
.body {
grid-area: 1 / 2;
overflow: auto;
}

View file

@ -1,33 +1,38 @@
import { Row, Column } from 'react-basics';
import { useRouter } from 'next/router';
import Script from 'next/script';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { useIntl, defineMessages } from 'react-intl';
import Link from 'components/common/Link';
import styles from './Footer.module.css';
import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants';
import styles from './Footer.module.css';
export default function Footer() {
const messages = defineMessages({
poweredBy: { id: 'message.powered-by', defaultMessage: 'Powered by {name}' },
});
export default function Footer({ className }) {
const { pathname } = useRouter();
const { formatMessage } = useIntl();
return (
<footer className={classNames(styles.footer, 'row')}>
<div className="col-12 col-md-4" />
<div className="col-12 col-md-4">
<FormattedMessage
id="message.powered-by"
defaultMessage="Powered by {name}"
values={{
name: (
<Link href={HOMEPAGE_URL}>
<b>umami</b>
</Link>
),
}}
/>
</div>
<div className={classNames(styles.version, 'col-12 col-md-4')}>
<Link href={REPO_URL}>{`v${CURRENT_VERSION}`}</Link>
</div>
<footer className={classNames(styles.footer, className)}>
<Row>
<Column>
<div>
{formatMessage(messages.poweredBy, {
name: (
<Link href={HOMEPAGE_URL}>
<b>umami</b>
</Link>
),
})}
</div>
</Column>
<Column className={styles.version}>
<Link href={REPO_URL}>{`v${CURRENT_VERSION}`}</Link>
</Column>
</Row>
{!pathname.includes('/share/') && <Script src={`/telemetry.js`} />}
</footer>
);

View file

@ -1,19 +1,17 @@
import Logo from 'assets/logo.svg';
import HamburgerButton from 'components/common/HamburgerButton';
import Link from 'components/common/Link';
import UpdateNotice from 'components/common/UpdateNotice';
import LanguageButton from 'components/settings/LanguageButton';
import ThemeButton from 'components/settings/ThemeButton';
import UserButton from 'components/settings/UserButton';
import LanguageButton from 'components/buttons/LanguageButton';
import ThemeButton from 'components/buttons/ThemeButton';
import UserButton from 'components/buttons/UserButton';
import useConfig from 'hooks/useConfig';
import useUser from 'hooks/useUser';
import { HOMEPAGE_URL } from 'lib/constants';
import { useRouter } from 'next/router';
import { Column, Icon, Row } from 'react-basics';
import SettingsButton from '../settings/SettingsButton';
import { Column, Row } from 'react-basics';
import SettingsButton from '../buttons/SettingsButton';
import styles from './Header.module.css';
import classNames from 'classnames';
export default function Header() {
export default function Header({ className }) {
const user = useUser();
const { pathname } = useRouter();
const { updatesDisabled, adminDisabled } = useConfig();
@ -23,14 +21,9 @@ export default function Header() {
return (
<>
{allowUpdate && <UpdateNotice />}
<header className={styles.header}>
<header className={classNames(styles.header, className)}>
<Row>
<Column className={styles.title}>
<Icon size="lg" className={styles.logo}>
<Logo />
</Icon>
<Link href={isSharePage ? HOMEPAGE_URL : '/'}>umami</Link>
</Column>
<Column className={styles.title}></Column>
<HamburgerButton />
<Column className={styles.buttons}>
<ThemeButton />

View file

@ -1,8 +1,9 @@
.header {
display: flex;
align-items: center;
min-height: 100px;
width: 100%;
height: 50px;
border-bottom: 1px solid var(--base300);
}
.title {

View file

@ -0,0 +1,48 @@
import { useState } from 'react';
import { Icon, Text, Icons } from 'react-basics';
import classNames from 'classnames';
import { Dashboard, Logo, Profile, User, Users, Clock, Globe } from 'components/icons';
import NavGroup from './NavGroup';
import styles from './NavBar.module.css';
const { ChevronDown, Search } = Icons;
const analytics = [
{ key: 'dashboard', label: 'Dashboard', url: '/dashboard', icon: <Dashboard /> },
{ key: 'realtime', label: 'Realtime', url: '/realtime', icon: <Clock /> },
{ key: 'queries', label: 'Queries', url: '/queries', icon: <Search /> },
];
const settings = [
{ key: 'websites', label: 'Websites', url: '/settings/websites', icon: <Globe /> },
{ key: 'users', label: 'Users', url: '/settings/users', icon: <User /> },
{ key: 'teams', label: 'Teams', url: '/settings/teams', icon: <Users /> },
];
export default function NavBar() {
const [minimized, setMinimized] = useState(false);
const handleMinimize = () => setMinimized(state => !state);
return (
<div className={classNames(styles.navbar, { [styles.minimized]: minimized })}>
<div className={styles.header} onClick={handleMinimize}>
<Icon size="lg">
<Logo />
</Icon>
<Text className={styles.text}>umami</Text>
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>
<ChevronDown />
</Icon>
</div>
<NavGroup title="Analytics" items={analytics} minimized={minimized} />
<NavGroup title="Settings" items={settings} minimized={minimized} />
<div className={styles.footer}>
<Icon>
<Profile />
</Icon>
<Text>Profile</Text>
</div>
</div>
);
}

View file

@ -0,0 +1,47 @@
.navbar {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
background: var(--base75);
height: 100%;
width: 200px;
border-right: 2px solid var(--base200);
}
.header {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 16px;
font-weight: 700;
padding: 20px 0;
cursor: pointer;
}
.header:hover .icon {
visibility: visible;
}
.icon {
position: absolute;
right: 0;
visibility: hidden;
}
.minimized.navbar {
width: 60px;
}
.minimized .text {
display: none;
}
.footer {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 20px;
}

View file

@ -0,0 +1,51 @@
import { useState } from 'react';
import { Icon, Text, Icons } from 'react-basics';
import classNames from 'classnames';
import { useRouter } from 'next/router';
import Link from 'next/link';
import styles from './NavGroup.module.css';
const { ChevronDown } = Icons;
export default function NavGroup({
title,
items,
defaultExpanded = true,
allowExpand = true,
minimized = false,
}) {
const { pathname } = useRouter();
const [expanded, setExpanded] = useState(defaultExpanded);
const handleExpand = () => setExpanded(state => !state);
return (
<div
className={classNames(styles.group, {
[styles.expanded]: expanded,
[styles.minimized]: minimized,
})}
>
<div className={styles.header} onClick={allowExpand ? handleExpand : undefined}>
<Text>{title}</Text>
<Icon size="sm" rotate={expanded ? 0 : -90}>
<ChevronDown />
</Icon>
</div>
<div className={styles.body}>
{items.map(({ key, label, url, icon }) => {
return (
<Link key={key} href={url}>
<a
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
>
<Icon>{icon}</Icon>
<Text className={styles.text}>{label}</Text>
</a>
</Link>
);
})}
</div>
</div>
);
}

View file

@ -0,0 +1,76 @@
.group {
display: flex;
flex-direction: column;
width: 100%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--base600);
font-size: 11px;
font-weight: 600;
padding: 10px 20px;
text-transform: uppercase;
cursor: pointer;
}
.body {
display: none;
}
.expanded .body {
display: block;
}
.items {
display: flex;
flex-direction: column;
gap: 20px;
margin-right: -2px;
width: 200px;
}
.item {
display: flex;
flex-direction: row;
align-items: center;
border-right: 2px solid var(--base200);
padding: 1rem 2rem;
gap: var(--size500);
font-weight: 600;
width: 200px;
}
.item.selected {
color: var(--base900);
border-right-color: var(--primary400);
background: var(--blue100);
}
.item:hover {
color: var(--base900);
}
.item.disabled {
color: var(--base500) !important;
pointer-events: none;
}
a.item {
color: var(--base600);
}
.minimized .text,
.minimized .header {
display: none;
}
.minimized .item {
width: 60px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -1,3 +1,4 @@
import React from 'react';
import Link from 'next/link';
import classNames from 'classnames';
import { Button, Icon } from 'react-basics';

View file

@ -1,48 +0,0 @@
import classNames from 'classnames';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Icon, Item, Menu, Text } from 'react-basics';
import useUser from 'hooks/useUser';
import User from 'assets/user.svg';
import Team from 'assets/users.svg';
import Website from 'assets/website.svg';
import Profile from 'assets/profile.svg';
import styles from './Nav.module.css';
export default function Nav() {
const user = useUser();
const { pathname } = useRouter();
if (!user) {
return null;
}
const handleSelect = () => {};
const items = [
{ icon: <Website />, label: 'Websites', url: '/settings/websites' },
{ icon: <User />, label: 'Users', url: '/settings/users' },
{ icon: <Team />, label: 'Teams', url: '/settings/teams' },
{ icon: <Profile />, label: 'Profile', url: '/settings/profile' },
];
return (
<Menu items={items} onSelect={handleSelect} className={styles.menu}>
{({ icon, label, url, hidden }) =>
!hidden && (
<Item
key={label}
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
>
<Link href={url}>
<a>
<Icon size="lg">{icon}</Icon>
<Text>{label}</Text>
</a>
</Link>
</Item>
)
}
</Menu>
);
}

View file

@ -1,45 +0,0 @@
.menu {
display: flex;
flex-direction: column;
width: 200px;
gap: 10px;
background: transparent;
margin-right: 16px;
}
.menu svg {
width: 20px;
height: 20px;
}
.item {
display: flex;
align-items: center;
gap: 20px;
font-weight: 600;
background: transparent;
padding: 0;
border-radius: 8px;
}
.item:hover {
background: var(--base100);
}
.item a {
color: var(--base600);
display: flex;
align-items: center;
gap: 20px;
flex: 1;
padding: 16px;
border-radius: 8px;
}
.item a:hover {
color: var(--base900);
}
.item.selected a {
color: var(--base900);
}

View file

@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
import DashboardSettingsButton from 'components/settings/DashboardSettingsButton';
import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
import styles from 'components/pages/websites/WebsiteList.module.css';
import useUser from 'hooks/useUser';

View file

@ -4,4 +4,5 @@
align-items: center;
justify-content: center;
height: 100vh;
background: var(--base75);
}

View file

@ -1,23 +0,0 @@
import AppLayout from 'components/layout/AppLayout';
import Menu from 'components/nav/Nav';
import styles from './SettingsLayout.module.css';
import useConfig from 'hooks/useConfig';
export default function SettingsLayout({ children }) {
const { adminDisabled } = useConfig();
if (adminDisabled) {
return null;
}
return (
<AppLayout>
<div className={styles.dashboard}>
<div className={styles.nav}>
<Menu />
</div>
<div className={styles.content}>{children}</div>
</div>
</AppLayout>
);
}

View file

@ -1,16 +0,0 @@
.dashboard {
display: flex;
flex: 1;
}
.nav {
margin-top: 20px;
}
.content {
position: relative;
background: var(--base50);
flex: 1;
border-radius: 8px;
overflow: hidden;
}

View file

@ -19,7 +19,13 @@ export default function LanguageSetting() {
return (
<Flexbox width={400} gap={10}>
<Dropdown items={options} value={locale} renderValue={renderValue} onChange={saveLocale}>
<Dropdown
items={options}
value={locale}
renderValue={renderValue}
onChange={saveLocale}
menuProps={{ style: { height: 300, width: 300 } }}
>
{item => <Item key={item}>{languages[item].label}</Item>}
</Dropdown>
<Button onClick={handleReset}>{formatMessage(messages.reset)}</Button>

View file

@ -1,9 +1,9 @@
import { Form, FormRow } from 'react-basics';
import { useIntl, defineMessages } from 'react-intl';
import TimezoneSetting from 'components/settings/TimezoneSetting';
import DateRangeSetting from 'components/settings/DateRangeSetting';
import LanguageSetting from 'components/settings/LanguageSetting';
import ThemeSetting from 'components/settings/ThemeSetting';
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
import ThemeSetting from 'components/buttons/ThemeSetting';
import useUser from 'hooks/useUser';
const messages = defineMessages({

View file

@ -1,6 +1,6 @@
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import ProfileDetails from 'components/settings/ProfileDetails';
import ProfileDetails from 'components/pages/settings/profile/ProfileDetails';
import { useState } from 'react';
import { Breadcrumbs, Icon, Item, useToast, Modal, Button } from 'react-basics';
import UserPasswordForm from 'components/pages/settings/users/UserPasswordForm';

View file

@ -17,7 +17,12 @@ export default function TimezoneSetting() {
return (
<Flexbox width={400} gap={10}>
<Dropdown items={options} value={timezone} onChange={saveTimezone}>
<Dropdown
items={options}
value={timezone}
onChange={saveTimezone}
menuProps={{ style: { height: 300, width: 300 } }}
>
{item => <Item key={item}>{item}</Item>}
</Dropdown>
<Button onClick={handleReset}>{formatMessage(messages.reset)}</Button>

View file

@ -1,5 +1,5 @@
import { useState } from 'react';
import { Button, Icon, Modal, useToast } from 'react-basics';
import { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics';
import useApi from 'hooks/useApi';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
@ -8,6 +8,8 @@ import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import Page from 'components/layout/Page';
import useUser from 'hooks/useUser';
const { Plus } = Icons;
export default function WebsitesList() {
const [edit, setEdit] = useState(false);
const { get, useQuery } = useApi();
@ -44,8 +46,11 @@ export default function WebsitesList() {
<Page loading={isLoading} error={error}>
{toast}
<PageHeader title="Websites">
<Button onClick={handleAdd}>
<Icon icon="plus" /> Add website
<Button variant="primary" onClick={handleAdd}>
<Icon>
<Plus />
</Icon>
<Text>Add website</Text>
</Button>
</PageHeader>
@ -53,7 +58,10 @@ export default function WebsitesList() {
{!hasData && (
<EmptyPlaceholder message="You don't have any websites configured.">
<Button variant="primary" onClick={handleAdd}>
<Icon icon="plus" /> Add website
<Icon>
<Plus />
</Icon>
<Text>Add website</Text>
</Button>
</EmptyPlaceholder>
)}

View file

@ -8,10 +8,12 @@ import {
TableColumn,
Button,
Icon,
Icons,
} from 'react-basics';
import ExternalLink from 'assets/external-link.svg';
import styles from './WebsitesTable.module.css';
const { ArrowRight, External } = Icons;
export default function WebsitesTable({ columns = [], rows = [] }) {
return (
<Table className={styles.table} columns={columns} rows={rows}>
@ -33,7 +35,9 @@ export default function WebsitesTable({ columns = [], rows = [] }) {
<Link href={`/settings/websites/${id}`}>
<a>
<Button>
<Icon icon="arrow-right" />
<Icon>
<ArrowRight />
</Icon>
Settings
</Button>
</a>
@ -42,7 +46,7 @@ export default function WebsitesTable({ columns = [], rows = [] }) {
<a>
<Button>
<Icon>
<ExternalLink />
<External />
</Icon>
View
</Button>

View file

@ -15,8 +15,8 @@ const messages = defineMessages({
defaultMessage: "You don't have any websites configured.",
},
goToSettngs: {
id: 'message.go-to-settings',
defaultMessage: 'Go to settings',
id: 'message.go-to-buttons',
defaultMessage: 'Go to buttons',
},
});