Rewrite admin. (#1713)

* Rewrite admin.

* Clean up password forms.

* Fix naming issues.

* CSS Naming.
This commit is contained in:
Brian Cao 2022-12-26 16:57:59 -08:00 committed by GitHub
parent f4db04c3c6
commit e1f99a7d01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
113 changed files with 2054 additions and 1872 deletions

View file

@ -3,6 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import MenuButton from 'components/common/MenuButton';
import Gear from 'assets/gear.svg';
import { saveDashboard } from 'store/dashboard';
import { Icon } from 'react-basics';
const messages = defineMessages({
toggleCharts: { id: 'message.toggle-charts', defaultMessage: 'Toggle charts' },
@ -32,5 +33,11 @@ export default function DashboardSettingsButton() {
}
}
return <MenuButton icon={<Gear />} options={menuOptions} onSelect={handleSelect} hideLabel />;
return (
<MenuButton options={menuOptions} onSelect={handleSelect} hideLabel>
<Icon>
<Gear />
</Icon>
</MenuButton>
);
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import DateFilter, { filterOptions } from 'components/common/DateFilter';
import Button from 'components/common/Button';
import { Button } from 'react-basics';
import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants';
import styles from './DateRangeSetting.module.css';
@ -28,7 +28,7 @@ export default function DateRangeSetting() {
endDate={endDate}
onChange={handleChange}
/>
<Button className={styles.button} size="small" onClick={handleReset}>
<Button className={styles.button} size="sm" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</>

View file

@ -4,6 +4,7 @@ import useLocale from 'hooks/useLocale';
import MenuButton from 'components/common/MenuButton';
import Globe from 'assets/globe.svg';
import styles from './LanguageButton.module.css';
import { Icon } from 'react-basics';
export default function LanguageButton() {
const { locale, saveLocale } = useLocale();
@ -15,13 +16,16 @@ export default function LanguageButton() {
return (
<MenuButton
icon={<Globe />}
options={menuOptions}
value={locale}
menuClassName={styles.menu}
buttonVariant="light"
onSelect={handleSelect}
hideLabel
/>
>
<Icon>
<Globe />
</Icon>
</MenuButton>
);
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import DropDown from 'components/common/DropDown';
import Button from 'components/common/Button';
import { Button } from 'react-basics';
import useLocale from 'hooks/useLocale';
import { DEFAULT_LOCALE } from 'lib/constants';
import styles from './TimezoneSetting.module.css';
@ -23,7 +23,7 @@ export default function LanguageSetting() {
options={options}
onChange={saveLocale}
/>
<Button className={styles.button} size="small" onClick={handleReset}>
<Button className={styles.button} size="sm" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</>

View file

@ -0,0 +1,53 @@
import TimezoneSetting from 'components/settings/TimezoneSetting';
import useUser from 'hooks/useUser';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import DateRangeSetting from './DateRangeSetting';
import LanguageSetting from './LanguageSetting';
import styles from './ProfileSettings.module.css';
import ThemeSetting from './ThemeSetting';
export default function ProfileDetails() {
const { user } = useUser();
if (!user) {
return null;
}
const { username } = user;
return (
<>
<dl className={styles.list}>
<dt>
<FormattedMessage id="label.username" defaultMessage="Username" />
</dt>
<dd>{username}</dd>
<dt>
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
</dt>
<dd>
<TimezoneSetting />
</dd>
<dt>
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
</dt>
<dd>
<DateRangeSetting />
</dd>
<dt>
<FormattedMessage id="label.language" defaultMessage="Language" />
</dt>
<dd>
<LanguageSetting />
</dd>
<dt>
<FormattedMessage id="label.theme" defaultMessage="Theme" />
</dt>
<dd>
<ThemeSetting />
</dd>
</dl>
</>
);
}

View file

@ -1,91 +0,0 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
import Modal from 'components/common/Modal';
import Toast from 'components/common/Toast';
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
import TimezoneSetting from 'components/settings/TimezoneSetting';
import Dots from 'assets/ellipsis-h.svg';
import styles from './ProfileSettings.module.css';
import DateRangeSetting from './DateRangeSetting';
import useEscapeKey from 'hooks/useEscapeKey';
import useUser from 'hooks/useUser';
import LanguageSetting from './LanguageSetting';
import ThemeSetting from './ThemeSetting';
export default function ProfileSettings() {
const { user } = useUser();
const [changePassword, setChangePassword] = useState(false);
const [message, setMessage] = useState(null);
function handleSave() {
setChangePassword(false);
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
}
useEscapeKey(() => {
setChangePassword(false);
});
if (!user) {
return null;
}
const { userId, username } = user;
return (
<>
<PageHeader>
<div>
<FormattedMessage id="label.profile" defaultMessage="Profile" />
</div>
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
<FormattedMessage id="label.change-password" defaultMessage="Change password" />
</Button>
</PageHeader>
<dl className={styles.list}>
<dt>
<FormattedMessage id="label.username" defaultMessage="Username" />
</dt>
<dd>{username}</dd>
<dt>
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
</dt>
<dd>
<TimezoneSetting />
</dd>
<dt>
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
</dt>
<dd>
<DateRangeSetting />
</dd>
<dt>
<FormattedMessage id="label.language" defaultMessage="Language" />
</dt>
<dd>
<LanguageSetting />
</dd>
<dt>
<FormattedMessage id="label.theme" defaultMessage="Theme" />
</dt>
<dd>
<ThemeSetting />
</dd>
</dl>
{changePassword && (
<Modal
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
>
<ChangePasswordForm
values={{ userId }}
onSave={handleSave}
onClose={() => setChangePassword(false)}
/>
</Modal>
)}
{message && <Toast message={message} onClose={() => setMessage(null)} />}
</>
);
}

View file

@ -2,7 +2,7 @@ import React, { useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import TimezoneSetting from './TimezoneSetting';
import DateRangeSetting from './DateRangeSetting';
import Button from 'components/common/Button';
import { Button, Icon } from 'react-basics';
import styles from './SettingsButton.module.css';
import Gear from 'assets/gear.svg';
import useDocumentClick from '../../hooks/useDocumentClick';
@ -23,7 +23,11 @@ export default function SettingsButton() {
return (
<div className={styles.button} ref={ref}>
<Button icon={<Gear />} variant="light" onClick={handleClick} />
<Button variant="light" onClick={handleClick}>
<Icon>
<Gear />
</Icon>
</Button>
{show && (
<div className={styles.panel}>
<dt>

View file

@ -4,7 +4,7 @@ import useTheme from 'hooks/useTheme';
import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg';
import styles from './ThemeButton.module.css';
import Icon from '../common/Icon';
import { Icon } from 'react-basics';
export default function ThemeButton() {
const [theme, setTheme] = useTheme();
@ -30,7 +30,7 @@ export default function ThemeButton() {
<div className={styles.button} onClick={handleClick}>
{transitions((styles, item) => (
<animated.div key={item} style={styles}>
<Icon icon={item === 'light' ? <Sun /> : <Moon />} />
<Icon>{item === 'light' ? <Sun /> : <Moon />}</Icon>
</animated.div>
))}
</div>

View file

@ -1,5 +1,5 @@
import classNames from 'classnames';
import Button from 'components/common/Button';
import { Button, Icon } from 'react-basics';
import useTheme from 'hooks/useTheme';
import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg';
@ -12,14 +12,20 @@ export default function ThemeSetting() {
<div className={styles.buttons}>
<Button
className={classNames({ [styles.active]: theme === 'light' })}
icon={<Sun />}
onClick={() => setTheme('light')}
/>
>
<Icon>
<Sun />
</Icon>
</Button>
<Button
className={classNames({ [styles.active]: theme === 'dark' })}
icon={<Moon />}
onClick={() => setTheme('dark')}
/>
>
<Icon>
<Moon />
</Icon>
</Button>
</div>
);
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { listTimeZones } from 'timezone-support';
import DropDown from 'components/common/DropDown';
import Button from 'components/common/Button';
import { Button } from 'react-basics';
import useTimezone from 'hooks/useTimezone';
import { getTimezone } from 'lib/date';
import styles from './TimezoneSetting.module.css';
@ -23,7 +23,7 @@ export default function TimezoneSetting() {
options={options}
onChange={saveTimezone}
/>
<Button className={styles.button} size="small" onClick={handleReset}>
<Button className={styles.button} size="sm" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</>

View file

@ -1,16 +1,18 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import { removeItem } from 'next-basics';
import MenuButton from 'components/common/MenuButton';
import Icon from 'components/common/Icon';
import User from 'assets/user.svg';
import styles from './UserButton.module.css';
import { AUTH_TOKEN } from 'lib/constants';
import useUser from 'hooks/useUser';
import useConfig from 'hooks/useConfig';
import useUser from 'hooks/useUser';
import { AUTH_TOKEN } from 'lib/constants';
import { removeItem } from 'next-basics';
import { useRouter } from 'next/router';
import React, { useRef, useState } from 'react';
import { Button, Icon, Item, Menu, Popup, Text } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import styles from './UserButton.module.css';
import useDocumentClick from '../../hooks/useDocumentClick';
export default function UserButton() {
const [show, setShow] = useState(false);
const ref = useRef();
const { user } = useUser();
const router = useRouter();
const { adminDisabled } = useConfig();
@ -31,26 +33,48 @@ export default function UserButton() {
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
value: 'profile',
hidden: adminDisabled,
divider: true,
},
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
];
function handleClick() {
setShow(state => !state);
}
function handleSelect(value) {
if (value === 'logout') {
removeItem(AUTH_TOKEN);
router.push('/login');
} else if (value === 'profile') {
router.push('/settings/profile');
router.push('/profile');
}
}
useDocumentClick(e => {
if (!ref.current?.contains(e.target)) {
setShow(false);
}
});
return (
<MenuButton
icon={<Icon icon={<User />} size="large" />}
buttonVariant="light"
options={menuOptions}
onSelect={handleSelect}
hideLabel
/>
<div className={styles.button} ref={ref}>
<Button variant="light" onClick={handleClick}>
<Icon className={styles.icon} size="large">
<User />
</Icon>
</Button>
{show && (
<Popup className={styles.menu} position="bottom" gap={5}>
<Menu items={menuOptions} onSelect={handleSelect}>
{({ label, value }) => (
<Item key={value}>
<Text>{label}</Text>
</Item>
)}
</Menu>
</Popup>
)}
</div>
);
}

View file

@ -1,3 +1,7 @@
.button {
position: relative;
}
.username {
border-bottom: 1px solid var(--base500);
}
@ -5,3 +9,18 @@
.username:hover {
background: var(--base50);
}
.icon svg {
font-size: 16px;
height: 16px;
width: 16px;
}
.menu {
left: -50%;
background: var(--base50);
border: 1px solid var(--base500);
border-radius: 4px;
overflow: hidden;
z-index: 100;
}

View file

@ -1,133 +0,0 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import Link from 'next/link';
import classNames from 'classnames';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
import Icon from 'components/common/Icon';
import Table from 'components/common/Table';
import Modal from 'components/common/Modal';
import Toast from 'components/common/Toast';
import UserEditForm from 'components/forms/UserEditForm';
import ButtonLayout from 'components/layout/ButtonLayout';
import DeleteForm from 'components/forms/DeleteForm';
import useFetch from 'hooks/useFetch';
import Pen from 'assets/pen.svg';
import Plus from 'assets/plus.svg';
import Trash from 'assets/trash.svg';
import Check from 'assets/check.svg';
import LinkIcon from 'assets/external-link.svg';
import styles from './UserSettings.module.css';
export default function UserSettings() {
const [addUser, setAddUser] = useState();
const [editUser, setEditUser] = useState();
const [deleteUser, setDeleteUser] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
const { data } = useFetch(`/users`, {}, [saved]);
const Checkmark = ({ isAdmin }) => (isAdmin ? <Icon icon={<Check />} size="medium" /> : null);
const DashboardLink = row => {
return (
<Link href={`/dashboard/${row.id}/${row.username}`}>
<a>
<Icon icon={<LinkIcon />} />
</a>
</Link>
);
};
const Buttons = row => (
<ButtonLayout align="right">
<Button icon={<Pen />} size="small" onClick={() => setEditUser(row)}>
<FormattedMessage id="label.edit" defaultMessage="Edit" />
</Button>
{!row.isAdmin && (
<Button icon={<Trash />} size="small" onClick={() => setDeleteUser(row)}>
<FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button>
)}
</ButtonLayout>
);
const columns = [
{
key: 'username',
label: <FormattedMessage id="label.username" defaultMessage="Username" />,
className: 'col-12 col-lg-4',
},
{
key: 'isAdmin',
label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />,
className: 'col-12 col-lg-3',
render: Checkmark,
},
{
key: 'dashboard',
label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />,
className: 'col-12 col-lg-3',
render: DashboardLink,
},
{
key: 'actions',
className: classNames(styles.buttons, 'col-12 col-lg-2 pt-2 pt-md-0'),
render: Buttons,
},
];
function handleSave() {
setSaved(state => state + 1);
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
handleClose();
}
function handleClose() {
setEditUser(null);
setAddUser(null);
setDeleteUser(null);
}
if (!data) {
return null;
}
return (
<>
<PageHeader>
<div>
<FormattedMessage id="label.users" defaultMessage="Users" />
</div>
<Button icon={<Plus />} size="small" onClick={() => setAddUser(true)}>
<FormattedMessage id="label.add-user" defaultMessage="Add user" />
</Button>
</PageHeader>
<Table columns={columns} rows={data} />
{editUser && (
<Modal title={<FormattedMessage id="label.edit-user" defaultMessage="Edit user" />}>
<UserEditForm
values={{ ...editUser, password: '' }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{addUser && (
<Modal title={<FormattedMessage id="label.add-user" defaultMessage="Add user" />}>
<UserEditForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{deleteUser && (
<Modal title={<FormattedMessage id="label.delete-user" defaultMessage="Delete user" />}>
<DeleteForm
values={{ type: 'users', id: deleteUser.id, name: deleteUser.username }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{message && <Toast message={message} onClose={() => setMessage(null)} />}
</>
);
}

View file

@ -1,5 +0,0 @@
.buttons {
display: flex;
justify-content: flex-end;
flex: 1;
}

View file

@ -1,232 +0,0 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Link from 'components/common/Link';
import Table from 'components/common/Table';
import Button from 'components/common/Button';
import OverflowText from 'components/common/OverflowText';
import PageHeader from 'components/layout/PageHeader';
import Modal from 'components/common/Modal';
import WebsiteEditForm from 'components/forms/WebsiteEditForm';
import ResetForm from 'components/forms/ResetForm';
import DeleteForm from 'components/forms/DeleteForm';
import TrackingCodeForm from 'components/forms/TrackingCodeForm';
import ShareUrlForm from 'components/forms/ShareUrlForm';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import ButtonLayout from 'components/layout/ButtonLayout';
import Toast from 'components/common/Toast';
import Favicon from 'components/common/Favicon';
import Pen from 'assets/pen.svg';
import Trash from 'assets/trash.svg';
import Reset from 'assets/redo.svg';
import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg';
import LinkIcon from 'assets/link.svg';
import useFetch from 'hooks/useFetch';
import useUser from 'hooks/useUser';
import styles from './WebsiteSettings.module.css';
export default function WebsiteSettings() {
const { user } = useUser();
const [editWebsite, setEditWebsite] = useState();
const [resetWebsite, setResetWebsite] = useState();
const [deleteWebsite, setDeleteWebsite] = useState();
const [addWebsite, setAddWebsite] = useState();
const [showCode, setShowCode] = useState();
const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
const { data } = useFetch('/websites', { params: { include_all: !!user?.isAdmin } }, [saved]);
const Buttons = row => (
<ButtonLayout align="right">
{row.shareId && (
<Button
icon={<LinkIcon />}
size="small"
tooltip={<FormattedMessage id="message.get-share-url" defaultMessage="Get share URL" />}
tooltipId={`button-share-${row.id}`}
onClick={() => setShowUrl(row)}
/>
)}
<Button
icon={<Code />}
size="small"
tooltip={
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
}
tooltipId={`button-code-${row.id}`}
onClick={() => setShowCode(row)}
/>
<Button
icon={<Pen />}
size="small"
tooltip={<FormattedMessage id="label.edit" defaultMessage="Edit" />}
tooltipId={`button-edit-${row.id}`}
onClick={() => setEditWebsite(row)}
/>
<Button
icon={<Reset />}
size="small"
tooltip={<FormattedMessage id="label.reset" defaultMessage="Reset" />}
tooltipId={`button-reset-${row.id}`}
onClick={() => setResetWebsite(row)}
/>
<Button
icon={<Trash />}
size="small"
tooltip={<FormattedMessage id="label.delete" defaultMessage="Delete" />}
tooltipId={`button-delete-${row.id}`}
onClick={() => setDeleteWebsite(row)}
/>
</ButtonLayout>
);
const DetailsLink = ({ id, name, domain }) => (
<Link className={styles.detailLink} href="/websites/[...id]" as={`/websites/${id}/${name}`}>
<Favicon domain={domain} />
<OverflowText tooltipId={`${id}-name`}>{name}</OverflowText>
</Link>
);
const Domain = ({ domain, id }) => (
<OverflowText tooltipId={`${id}-domain`}>{domain}</OverflowText>
);
const adminColumns = [
{
key: 'name',
label: <FormattedMessage id="label.name" defaultMessage="Name" />,
className: 'col-12 col-lg-4 col-xl-3',
render: DetailsLink,
},
{
key: 'domain',
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
className: 'col-12 col-lg-4 col-xl-3',
render: Domain,
},
{
key: 'user',
label: <FormattedMessage id="label.owner" defaultMessage="Owner" />,
className: 'col-12 col-lg-4 col-xl-1',
},
{
key: 'action',
className: classNames(styles.buttons, 'col-12 col-xl-5 pt-2 pt-xl-0'),
render: Buttons,
},
];
const columns = [
{
key: 'name',
label: <FormattedMessage id="label.name" defaultMessage="Name" />,
className: 'col-12 col-lg-6 col-xl-4',
render: DetailsLink,
},
{
key: 'domain',
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
className: 'col-12 col-lg-6 col-xl-4',
render: Domain,
},
{
key: 'action',
className: classNames(styles.buttons, 'col-12 col-xl-4 pt-2 pt-xl-0'),
render: Buttons,
},
];
function handleSave() {
setSaved(state => state + 1);
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
handleClose();
}
function handleClose() {
setAddWebsite(null);
setEditWebsite(null);
setResetWebsite(null);
setDeleteWebsite(null);
setShowCode(null);
setShowUrl(null);
}
if (!data) {
return null;
}
const empty = (
<EmptyPlaceholder
msg={
<FormattedMessage
id="message.no-websites-configured"
defaultMessage="You don't have any websites configured."
/>
}
>
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}>
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
</Button>
</EmptyPlaceholder>
);
return (
<>
<PageHeader>
<div>
<FormattedMessage id="label.websites" defaultMessage="Websites" />
</div>
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
</Button>
</PageHeader>
<Table columns={user.isAdmin ? adminColumns : columns} rows={data} empty={empty} />
{editWebsite && (
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{addWebsite && (
<Modal title={<FormattedMessage id="label.add-website" defaultMessage="Add website" />}>
<WebsiteEditForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{resetWebsite && (
<Modal
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
>
<ResetForm
values={{ type: 'websites', id: resetWebsite.id, name: resetWebsite.name }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{deleteWebsite && (
<Modal
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
>
<DeleteForm
values={{ type: 'websites', id: deleteWebsite.id, name: deleteWebsite.name }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{showCode && (
<Modal title={<FormattedMessage id="label.tracking-code" defaultMessage="Tracking code" />}>
<TrackingCodeForm values={showCode} onClose={handleClose} />
</Modal>
)}
{showUrl && (
<Modal title={<FormattedMessage id="label.share-url" defaultMessage="Share URL" />}>
<ShareUrlForm values={showUrl} onClose={handleClose} />
</Modal>
)}
{message && <Toast message={message} onClose={() => setMessage(null)} />}
</>
);
}

View file

@ -1,13 +0,0 @@
.col {
flex: 2;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
.detailLink {
width: 100%;
}