mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 09:45:04 +01:00
Update teams features.
This commit is contained in:
parent
89f2fd601e
commit
656df4f846
@ -1,14 +1,17 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Icon, Icons } from 'react-basics';
|
import { Icon, Icons, Text } from 'react-basics';
|
||||||
|
import { messages } from 'components/messages';
|
||||||
import styles from './ErrorMessage.module.css';
|
import styles from './ErrorMessage.module.css';
|
||||||
|
|
||||||
export default function ErrorMessage() {
|
export default function ErrorMessage() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
<Icon className={styles.icon} size="large">
|
<Icon className={styles.icon} size="large">
|
||||||
<Icons.Alert />
|
<Icons.Alert />
|
||||||
</Icon>
|
</Icon>
|
||||||
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
|
<Text>{formatMessage(messages.error)}</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Icons } from 'react-basics';
|
import { Icons } from 'react-basics';
|
||||||
|
import AddUser from 'assets/add-user.svg';
|
||||||
import Bolt from 'assets/bolt.svg';
|
import Bolt from 'assets/bolt.svg';
|
||||||
import Calendar from 'assets/calendar.svg';
|
import Calendar from 'assets/calendar.svg';
|
||||||
import Clock from 'assets/clock.svg';
|
import Clock from 'assets/clock.svg';
|
||||||
@ -15,6 +16,7 @@ import Users from 'assets/users.svg';
|
|||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
...Icons,
|
...Icons,
|
||||||
|
AddUser,
|
||||||
Bolt,
|
Bolt,
|
||||||
Calendar,
|
Calendar,
|
||||||
Clock,
|
Clock,
|
||||||
|
@ -26,8 +26,11 @@ export const labels = defineMessages({
|
|||||||
team: { id: 'label.team', defaultMessage: 'Team' },
|
team: { id: 'label.team', defaultMessage: 'Team' },
|
||||||
regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' },
|
regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' },
|
||||||
remove: { id: 'label.remove', defaultMessage: 'Remove' },
|
remove: { id: 'label.remove', defaultMessage: 'Remove' },
|
||||||
|
join: { id: 'label.join', defaultMessage: 'Join' },
|
||||||
createTeam: { id: 'label.create-team', defaultMessage: 'Create team' },
|
createTeam: { id: 'label.create-team', defaultMessage: 'Create team' },
|
||||||
|
joinTeam: { id: 'label.join-team', defaultMessage: 'Join team' },
|
||||||
settings: { id: 'label.settings', defaultMessage: 'Settings' },
|
settings: { id: 'label.settings', defaultMessage: 'Settings' },
|
||||||
|
owner: { id: 'label.owner', defaultMessage: 'Owner' },
|
||||||
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
|
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
|
||||||
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
|
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
|
||||||
teamGuest: { id: 'label.team-guest', defaultMessage: 'Team guest' },
|
teamGuest: { id: 'label.team-guest', defaultMessage: 'Team guest' },
|
||||||
@ -61,6 +64,7 @@ export const labels = defineMessages({
|
|||||||
logout: { id: 'label.logout', defaultMessage: 'Logout' },
|
logout: { id: 'label.logout', defaultMessage: 'Logout' },
|
||||||
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
|
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
|
||||||
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
||||||
|
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
@ -79,7 +83,7 @@ export const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
noTeams: {
|
noTeams: {
|
||||||
id: 'message.no-teams',
|
id: 'message.no-teams',
|
||||||
defaultMessage: 'You have no created any teams.',
|
defaultMessage: 'You have not created any teams.',
|
||||||
},
|
},
|
||||||
shareUrl: {
|
shareUrl: {
|
||||||
id: 'message.share-url',
|
id: 'message.share-url',
|
||||||
@ -120,6 +124,14 @@ export const messages = defineMessages({
|
|||||||
id: 'message.go-to-settings',
|
id: 'message.go-to-settings',
|
||||||
defaultMessage: 'Go to settings',
|
defaultMessage: 'Go to settings',
|
||||||
},
|
},
|
||||||
|
activeUsers: {
|
||||||
|
id: 'message.active-users',
|
||||||
|
defaultMessage: '{x} current {x, plural, one {visitor} other {visitors}}',
|
||||||
|
},
|
||||||
|
teamNotFound: {
|
||||||
|
id: 'message.team-not-found',
|
||||||
|
defaultMessage: 'Team not found.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const devices = defineMessages({
|
export const devices = defineMessages({
|
||||||
@ -129,6 +141,12 @@ export const devices = defineMessages({
|
|||||||
mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' },
|
mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function getMessage(id, formatMessage) {
|
||||||
|
const message = Object.values(messages).find(value => value.id === id);
|
||||||
|
|
||||||
|
return message ? formatMessage(message) : id;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDeviceMessage(device) {
|
export function getDeviceMessage(device) {
|
||||||
return devices[device] || labels.unknown;
|
return devices[device] || labels.unknown;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { StatusLight } from 'react-basics';
|
import { StatusLight } from 'react-basics';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
import { messages } from 'components/messages';
|
||||||
import styles from './ActiveUsers.module.css';
|
import styles from './ActiveUsers.module.css';
|
||||||
|
|
||||||
export default function ActiveUsers({ websiteId, className, value, refetchInterval = 60000 }) {
|
export default function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
|
||||||
const url = websiteId ? `/websites/${websiteId}/active` : null;
|
const { formatMessage } = useIntl();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data } = useQuery(['websites:active', websiteId], () => get(url), {
|
const { data } = useQuery(
|
||||||
|
['websites:active', websiteId],
|
||||||
|
() => get(`/websites/${websiteId}/active`),
|
||||||
|
{
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const count = useMemo(() => {
|
const count = useMemo(() => {
|
||||||
if (websiteId) {
|
if (websiteId) {
|
||||||
@ -25,17 +29,8 @@ export default function ActiveUsers({ websiteId, className, value, refetchInterv
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<StatusLight variant="success">
|
||||||
<StatusLight variant="success" />
|
<div className={styles.text}>{formatMessage(messages.activeUsers, { x: count })}</div>
|
||||||
<div className={styles.text}>
|
</StatusLight>
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
id="message.active-users"
|
|
||||||
defaultMessage="{x} current {x, plural, one {visitor} other {visitors}}"
|
|
||||||
values={{ x: count }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,9 @@ export default function ChartTooltip({ chartId, tooltip }) {
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
<div className={styles.metric}>
|
<div className={styles.metric}>
|
||||||
<StatusLight color={labelColor} />
|
<StatusLight color={labelColor}>
|
||||||
{value} {label}
|
{value} {label}
|
||||||
|
</StatusLight>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,8 +34,9 @@ export default function Legend({ chart }) {
|
|||||||
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
||||||
onClick={() => handleClick(datasetIndex)}
|
onClick={() => handleClick(datasetIndex)}
|
||||||
>
|
>
|
||||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()} />
|
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
||||||
<span className={locale}>{text}</span>
|
<span className={locale}>{text}</span>
|
||||||
|
</StatusLight>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Row, Column, Loading } from 'react-basics';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Button, Icon, Text, Row, Column, Loading } from 'react-basics';
|
||||||
|
import Link from 'next/link';
|
||||||
import PageviewsChart from './PageviewsChart';
|
import PageviewsChart from './PageviewsChart';
|
||||||
import MetricsBar from './MetricsBar';
|
import MetricsBar from './MetricsBar';
|
||||||
import WebsiteHeader from './WebsiteHeader';
|
import WebsiteHeader from './WebsiteHeader';
|
||||||
@ -12,6 +14,8 @@ import useDateRange from 'hooks/useDateRange';
|
|||||||
import useTimezone from 'hooks/useTimezone';
|
import useTimezone from 'hooks/useTimezone';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
|
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
import styles from './WebsiteChart.module.css';
|
import styles from './WebsiteChart.module.css';
|
||||||
|
|
||||||
export default function WebsiteChart({
|
export default function WebsiteChart({
|
||||||
@ -20,8 +24,10 @@ export default function WebsiteChart({
|
|||||||
domain,
|
domain,
|
||||||
stickyHeader = false,
|
stickyHeader = false,
|
||||||
showChart = true,
|
showChart = true,
|
||||||
|
showDetailsButton = false,
|
||||||
onDataLoad = () => {},
|
onDataLoad = () => {},
|
||||||
}) {
|
}) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||||
const { startDate, endDate, unit, value, modified } = dateRange;
|
const { startDate, endDate, unit, value, modified } = dateRange;
|
||||||
const [timezone] = useTimezone();
|
const [timezone] = useTimezone();
|
||||||
@ -82,7 +88,20 @@ export default function WebsiteChart({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WebsiteHeader websiteId={websiteId} title={title} domain={domain} />
|
<WebsiteHeader websiteId={websiteId} title={title} domain={domain}>
|
||||||
|
{showDetailsButton && (
|
||||||
|
<Link href={`/websites/${websiteId}`}>
|
||||||
|
<a>
|
||||||
|
<Button>
|
||||||
|
<Text>{formatMessage(labels.viewDetails)}</Text>
|
||||||
|
<Icon>
|
||||||
|
<Icons.ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</WebsiteHeader>
|
||||||
<StickyHeader
|
<StickyHeader
|
||||||
className={styles.metrics}
|
className={styles.metrics}
|
||||||
stickyClassName={styles.sticky}
|
stickyClassName={styles.sticky}
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
import { Row, Column } from 'react-basics';
|
import { Row, Column } from 'react-basics';
|
||||||
import Favicon from 'components/common/Favicon';
|
import Favicon from 'components/common/Favicon';
|
||||||
import OverflowText from 'components/common/OverflowText';
|
import OverflowText from 'components/common/OverflowText';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
|
||||||
import ActiveUsers from './ActiveUsers';
|
import ActiveUsers from './ActiveUsers';
|
||||||
import styles from './WebsiteHeader.module.css';
|
import styles from './WebsiteHeader.module.css';
|
||||||
|
|
||||||
export default function WebsiteHeader({ websiteId, title, domain }) {
|
export default function WebsiteHeader({ websiteId, title, domain, children }) {
|
||||||
return (
|
return (
|
||||||
<PageHeader>
|
<Row className={styles.header} justifyContent="center">
|
||||||
<Row>
|
|
||||||
<Column className={styles.title} variant="two">
|
<Column className={styles.title} variant="two">
|
||||||
<Favicon domain={domain} />
|
<Favicon domain={domain} />
|
||||||
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
||||||
</Column>
|
</Column>
|
||||||
<Column className={styles.active} variant="two">
|
<Column className={styles.body} variant="two">
|
||||||
<ActiveUsers websiteId={websiteId} />
|
<ActiveUsers websiteId={websiteId} />
|
||||||
|
{children}
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
</PageHeader>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
|
.header {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: var(--font-size-lg);
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
|
gap: 30px;
|
||||||
}
|
}
|
||||||
|
43
components/pages/settings/teams/JoinTeamForm.js
Normal file
43
components/pages/settings/teams/JoinTeamForm.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormRow,
|
||||||
|
FormInput,
|
||||||
|
FormButtons,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
SubmitButton,
|
||||||
|
} from 'react-basics';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
import { labels, getMessage } from 'components/messages';
|
||||||
|
|
||||||
|
export default function TeamJoinForm({ onSave, onClose }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { post, useMutation } = useApi();
|
||||||
|
const { mutate, error } = useMutation(data => post('/teams/join', data));
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
const handleSubmit = async data => {
|
||||||
|
mutate(data, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
onSave();
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form ref={ref} onSubmit={handleSubmit} error={error && getMessage(error, formatMessage)}>
|
||||||
|
<FormRow label={formatMessage(labels.accessCode)}>
|
||||||
|
<FormInput name="accessCode" rules={{ required: formatMessage(labels.required) }}>
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
|
<FormButtons flex>
|
||||||
|
<SubmitButton variant="primary">{formatMessage(labels.join)}</SubmitButton>
|
||||||
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
@ -4,7 +4,7 @@ import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
|||||||
|
|
||||||
export default function TeamMembers({ teamId }) {
|
export default function TeamMembers({ teamId }) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading } = useQuery(['team-members', teamId], () =>
|
const { data, isLoading } = useQuery(['teams:users', teamId], () =>
|
||||||
get(`/teams/${teamId}/users`),
|
get(`/teams/${teamId}/users`),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -40,14 +40,16 @@ export default function TeamSettings({ teamId }) {
|
|||||||
return (
|
return (
|
||||||
<Page loading={isLoading || !values}>
|
<Page loading={isLoading || !values}>
|
||||||
{toast}
|
{toast}
|
||||||
<PageHeader>
|
<PageHeader
|
||||||
|
title={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Item>
|
<Item>
|
||||||
<Link href="/settings/teams">Teams</Link>
|
<Link href="/settings/teams">Teams</Link>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>{values?.name}</Item>
|
<Item>{values?.name}</Item>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</PageHeader>
|
}
|
||||||
|
/>
|
||||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
||||||
<Item key="details">{formatMessage(labels.details)}</Item>
|
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||||
<Item key="members">{formatMessage(labels.members)}</Item>
|
<Item key="members">{formatMessage(labels.members)}</Item>
|
||||||
|
@ -7,7 +7,7 @@ import { messages } from 'components/messages';
|
|||||||
export default function TeamWebsites({ teamId }) {
|
export default function TeamWebsites({ teamId }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading } = useQuery(['teams/websites', teamId], () =>
|
const { data, isLoading } = useQuery(['teams:websites', teamId], () =>
|
||||||
get(`/teams/${teamId}/websites`),
|
get(`/teams/${teamId}/websites`),
|
||||||
);
|
);
|
||||||
const hasData = data && data.length !== 0;
|
const hasData = data && data.length !== 0;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Icon, Modal, ModalTrigger, useToast, Icons, Text } from 'react-basics';
|
import { Button, Icon, Modal, ModalTrigger, useToast, Text, Flexbox } from 'react-basics';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
@ -8,6 +8,8 @@ import PageHeader from 'components/layout/PageHeader';
|
|||||||
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import { labels, messages } from 'components/messages';
|
import { labels, messages } from 'components/messages';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import TeamJoinForm from './JoinTeamForm';
|
||||||
|
|
||||||
export default function TeamsList() {
|
export default function TeamsList() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -22,6 +24,24 @@ export default function TeamsList() {
|
|||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleJoin = () => {
|
||||||
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const joinButton = (
|
||||||
|
<ModalTrigger>
|
||||||
|
<Button variant="secondary">
|
||||||
|
<Icon>
|
||||||
|
<Icons.AddUser />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.joinTeam)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal title={formatMessage(labels.joinTeam)}>
|
||||||
|
{close => <TeamJoinForm onSave={handleJoin} onClose={close} />}
|
||||||
|
</Modal>
|
||||||
|
</ModalTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
const createButton = (
|
const createButton = (
|
||||||
<ModalTrigger>
|
<ModalTrigger>
|
||||||
<Button variant="primary">
|
<Button variant="primary">
|
||||||
@ -39,7 +59,14 @@ export default function TeamsList() {
|
|||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<Page loading={isLoading} error={error}>
|
||||||
{toast}
|
{toast}
|
||||||
<PageHeader title={formatMessage(labels.team)}>{createButton}</PageHeader>
|
<PageHeader title={formatMessage(labels.team)}>
|
||||||
|
{hasData && (
|
||||||
|
<Flexbox gap={10}>
|
||||||
|
{joinButton}
|
||||||
|
{createButton}
|
||||||
|
</Flexbox>
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
{hasData && <TeamsTable data={data} />}
|
{hasData && <TeamsTable data={data} />}
|
||||||
{!hasData && (
|
{!hasData && (
|
||||||
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
||||||
|
@ -14,12 +14,14 @@ import {
|
|||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
|
import { ROLES } from 'lib/constants';
|
||||||
|
|
||||||
export default function TeamsTable({ data = [] }) {
|
export default function TeamsTable({ data = [] }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
|
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
|
||||||
|
{ name: 'owner', label: formatMessage(labels.owner) },
|
||||||
{ name: 'action', label: ' ' },
|
{ name: 'action', label: ' ' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -38,7 +40,10 @@ export default function TeamsTable({ data = [] }) {
|
|||||||
{(row, keys, rowIndex) => {
|
{(row, keys, rowIndex) => {
|
||||||
const { id } = row;
|
const { id } = row;
|
||||||
|
|
||||||
row.action = (
|
const rowData = {
|
||||||
|
...row,
|
||||||
|
owner: row.teamUsers.find(({ role }) => role === ROLES.teamOwner)?.user?.username,
|
||||||
|
action: (
|
||||||
<Flexbox flex={1} justifyContent="end">
|
<Flexbox flex={1} justifyContent="end">
|
||||||
<Link href={`/settings/teams/${id}`}>
|
<Link href={`/settings/teams/${id}`}>
|
||||||
<a>
|
<a>
|
||||||
@ -51,10 +56,11 @@ export default function TeamsTable({ data = [] }) {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
<TableRow key={rowIndex} data={rowData} keys={keys}>
|
||||||
{(data, key, colIndex) => {
|
{(data, key, colIndex) => {
|
||||||
return (
|
return (
|
||||||
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
||||||
|
@ -46,14 +46,16 @@ export default function UserSettings({ userId }) {
|
|||||||
return (
|
return (
|
||||||
<Page loading={isLoading || !values}>
|
<Page loading={isLoading || !values}>
|
||||||
{toast}
|
{toast}
|
||||||
<PageHeader>
|
<PageHeader
|
||||||
|
title={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Item>
|
<Item>
|
||||||
<Link href="/settings/users">{formatMessage(labels.users)}</Link>
|
<Link href="/settings/users">{formatMessage(labels.users)}</Link>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>{values?.username}</Item>
|
<Item>{values?.username}</Item>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</PageHeader>
|
}
|
||||||
|
/>
|
||||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||||
<Item key="details">{formatMessage(labels.details)}</Item>
|
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||||
<Item key="websites">{formatMessage(labels.websites)}</Item>
|
<Item key="websites">{formatMessage(labels.websites)}</Item>
|
||||||
|
@ -7,7 +7,7 @@ import { messages } from 'components/messages';
|
|||||||
export default function UserWebsites({ userId }) {
|
export default function UserWebsites({ userId }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading } = useQuery(['user/websites', userId], () =>
|
const { data, isLoading } = useQuery(['user:websites', userId], () =>
|
||||||
get(`/users/${userId}/websites`),
|
get(`/users/${userId}/websites`),
|
||||||
);
|
);
|
||||||
const hasData = data && data.length !== 0;
|
const hasData = data && data.length !== 0;
|
||||||
|
@ -49,13 +49,16 @@ export default function WebsiteSettings({ websiteId }) {
|
|||||||
return (
|
return (
|
||||||
<Page loading={isLoading || !values}>
|
<Page loading={isLoading || !values}>
|
||||||
{toast}
|
{toast}
|
||||||
<PageHeader>
|
<PageHeader
|
||||||
|
title={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Item>
|
<Item>
|
||||||
<Link href="/settings/websites">{formatMessage(labels.websites)}</Link>
|
<Link href="/settings/websites">{formatMessage(labels.websites)}</Link>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>{values?.name}</Item>
|
<Item>{values?.name}</Item>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Link href={`/websites/${websiteId}`}>
|
<Link href={`/websites/${websiteId}`}>
|
||||||
<a>
|
<a>
|
||||||
<Button variant="primary">
|
<Button variant="primary">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
|
||||||
import styles from './WebsiteList.module.css';
|
|
||||||
import useDashboard from 'store/dashboard';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { firstBy } from 'thenby';
|
import { firstBy } from 'thenby';
|
||||||
|
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||||
|
import useDashboard from 'store/dashboard';
|
||||||
|
import styles from './WebsiteList.module.css';
|
||||||
|
|
||||||
export default function WebsiteList({ websites, showCharts, limit }) {
|
export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||||
const { websiteOrder } = useDashboard();
|
const { websiteOrder } = useDashboard();
|
||||||
|
|
||||||
const ordered = useMemo(
|
const ordered = useMemo(
|
||||||
@ -17,19 +17,19 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ordered.map(({ id, name, domain }, index) =>
|
{ordered.map(({ id, name, domain }, index) => {
|
||||||
index < limit ? (
|
return index < limit ? (
|
||||||
<div key={id} className={styles.website}>
|
<div key={id} className={styles.website}>
|
||||||
<WebsiteChart
|
<WebsiteChart
|
||||||
websiteId={id}
|
websiteId={id}
|
||||||
title={name}
|
title={name}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
showChart={showCharts}
|
showChart={showCharts}
|
||||||
showLink
|
showDetailsButton={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null,
|
) : null;
|
||||||
)}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -38,11 +38,11 @@ export default async (
|
|||||||
|
|
||||||
if (user && checkPassword(password, user.password)) {
|
if (user && checkPassword(password, user.password)) {
|
||||||
if (redis.enabled) {
|
if (redis.enabled) {
|
||||||
const key = `auth:${getRandomChars(32)}`;
|
const authKey = `auth:${getRandomChars(32)}`;
|
||||||
|
|
||||||
await redis.set(key, user);
|
await redis.set(authKey, user);
|
||||||
|
|
||||||
const token = createSecureToken({ key }, secret());
|
const token = createSecureToken({ authKey }, secret());
|
||||||
|
|
||||||
return ok(res, { token, user });
|
return ok(res, { token, user });
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { canCreateTeam } from 'lib/auth';
|
|||||||
import { uuid } from 'lib/crypto';
|
import { uuid } from 'lib/crypto';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { createTeam, getUserTeams } from 'queries';
|
import { createTeam, getUserTeams } from 'queries';
|
||||||
|
|
||||||
export interface TeamsRequestBody {
|
export interface TeamsRequestBody {
|
||||||
@ -34,13 +34,14 @@ export default async (
|
|||||||
|
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
const created = await createTeam({
|
const team = await createTeam({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
name,
|
name,
|
||||||
userId,
|
userId,
|
||||||
|
accessCode: getRandomChars(16),
|
||||||
});
|
});
|
||||||
|
|
||||||
return ok(res, created);
|
return ok(res, team);
|
||||||
}
|
}
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
return methodNotAllowed(res);
|
||||||
|
34
pages/api/teams/join.ts
Normal file
34
pages/api/teams/join.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Team } from '@prisma/client';
|
||||||
|
import { NextApiRequestQueryBody } from 'lib/types';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { methodNotAllowed, ok, notFound } from 'next-basics';
|
||||||
|
import { createTeamUser, getTeam } from 'queries';
|
||||||
|
import { ROLES } from 'lib/constants';
|
||||||
|
|
||||||
|
export interface TeamsRequestBody {
|
||||||
|
accessCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
req: NextApiRequestQueryBody<any, TeamsRequestBody>,
|
||||||
|
res: NextApiResponse<Team[] | Team>,
|
||||||
|
) => {
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const { accessCode } = req.body;
|
||||||
|
|
||||||
|
const team = await getTeam({ accessCode });
|
||||||
|
|
||||||
|
if (!team) {
|
||||||
|
return notFound(res, 'message.team-not-found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember);
|
||||||
|
|
||||||
|
return ok(res, team);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
@ -47,17 +47,21 @@ export async function getUsers(): Promise<User[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserTeams(userId: string): Promise<Team[]> {
|
export async function getUserTeams(userId: string): Promise<Team[]> {
|
||||||
return prisma.client.teamUser
|
return prisma.client.team.findMany({
|
||||||
.findMany({
|
|
||||||
where: {
|
where: {
|
||||||
|
teamUsers: {
|
||||||
|
some: {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
include: {
|
|
||||||
team: true,
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
.then(data => {
|
include: {
|
||||||
return data.map(a => a.team);
|
teamUsers: {
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user