mirror of
https://github.com/umami-software/umami.git
synced 2026-02-12 08:37:13 +01:00
Renamed (app) folder to (main).
This commit is contained in:
parent
5c15778c9b
commit
c990459238
167 changed files with 48 additions and 114 deletions
27
src/app/(main)/settings/users/UserAddButton.js
Normal file
27
src/app/(main)/settings/users/UserAddButton.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics';
|
||||
import UserAddForm from './UserAddForm';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function UserAddButton({ onSave }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSave = () => {
|
||||
onSave();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createUser)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.createUser)}>
|
||||
{close => <UserAddForm onSave={handleSave} onClose={close} />}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserAddButton;
|
||||
76
src/app/(main)/settings/users/UserAddForm.js
Normal file
76
src/app/(main)/settings/users/UserAddForm.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
Dropdown,
|
||||
Item,
|
||||
Form,
|
||||
FormRow,
|
||||
FormButtons,
|
||||
FormInput,
|
||||
TextField,
|
||||
PasswordField,
|
||||
SubmitButton,
|
||||
Button,
|
||||
} from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function UserAddForm({ onSave, onClose }) {
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isLoading } = useMutation(data => post(`/users`, data));
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave(data);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderValue = value => {
|
||||
if (value === ROLES.user) {
|
||||
return formatMessage(labels.user);
|
||||
}
|
||||
if (value === ROLES.admin) {
|
||||
return formatMessage(labels.admin);
|
||||
}
|
||||
if (value === ROLES.viewOnly) {
|
||||
return formatMessage(labels.viewOnly);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error}>
|
||||
<FormRow label={formatMessage(labels.username)}>
|
||||
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}>
|
||||
<TextField autoComplete="new-username" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.password)}>
|
||||
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}>
|
||||
<PasswordField autoComplete="new-password" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.role)}>
|
||||
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
||||
<Dropdown renderValue={renderValue}>
|
||||
<Item key={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</Item>
|
||||
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
|
||||
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
|
||||
</Dropdown>
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="primary" disabled={false}>
|
||||
{formatMessage(labels.save)}
|
||||
</SubmitButton>
|
||||
<Button disabled={isLoading} onClick={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserAddForm;
|
||||
27
src/app/(main)/settings/users/UserDeleteButton.js
Normal file
27
src/app/(main)/settings/users/UserDeleteButton.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import UserDeleteForm from './UserDeleteForm';
|
||||
|
||||
export function UserDeleteButton({ userId, username, onDelete }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ModalTrigger disabled={userId === user?.id}>
|
||||
<Button disabled={userId === user?.id}>
|
||||
<Icon>
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.deleteUser)}>
|
||||
{close => (
|
||||
<UserDeleteForm userId={userId} username={username} onSave={onDelete} onClose={close} />
|
||||
)}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserDeleteButton;
|
||||
37
src/app/(main)/settings/users/UserDeleteForm.js
Normal file
37
src/app/(main)/settings/users/UserDeleteForm.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function UserDeleteForm({ userId, username, onSave, onClose }) {
|
||||
const { formatMessage, FormattedMessage, labels, messages } = useMessages();
|
||||
const { del } = useApi();
|
||||
const { mutate, error, isLoading } = useMutation(() => del(`/users/${userId}`));
|
||||
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave();
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error}>
|
||||
<p>
|
||||
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{username}</b> }} />
|
||||
</p>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="danger" disabled={isLoading}>
|
||||
{formatMessage(labels.delete)}
|
||||
</SubmitButton>
|
||||
<Button disabled={isLoading} onClick={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserDeleteForm;
|
||||
76
src/app/(main)/settings/users/UserEditForm.js
Normal file
76
src/app/(main)/settings/users/UserEditForm.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
Dropdown,
|
||||
Item,
|
||||
Form,
|
||||
FormRow,
|
||||
FormButtons,
|
||||
FormInput,
|
||||
TextField,
|
||||
SubmitButton,
|
||||
PasswordField,
|
||||
} from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function UserEditForm({ userId, data, onSave }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error } = useMutation(({ username, password, role }) =>
|
||||
post(`/users/${userId}`, { username, password, role }),
|
||||
);
|
||||
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderValue = value => {
|
||||
if (value === ROLES.user) {
|
||||
return formatMessage(labels.user);
|
||||
}
|
||||
if (value === ROLES.admin) {
|
||||
return formatMessage(labels.admin);
|
||||
}
|
||||
if (value === ROLES.viewOnly) {
|
||||
return formatMessage(labels.viewOnly);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error} values={data} style={{ width: 300 }}>
|
||||
<FormRow label={formatMessage(labels.username)}>
|
||||
<FormInput name="username">
|
||||
<TextField />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.password)}>
|
||||
<FormInput
|
||||
name="password"
|
||||
rules={{
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="new-password" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.role)}>
|
||||
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
||||
<Dropdown renderValue={renderValue}>
|
||||
<Item key={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</Item>
|
||||
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
|
||||
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
|
||||
</Dropdown>
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserEditForm;
|
||||
36
src/app/(main)/settings/users/UserWebsites.js
Normal file
36
src/app/(main)/settings/users/UserWebsites.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Page from 'components/layout/Page';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
|
||||
import useApiFilter from 'components/hooks/useApiFilter';
|
||||
|
||||
export function UserWebsites({ userId }) {
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(
|
||||
['user:websites', userId, filter, page, pageSize],
|
||||
() =>
|
||||
get(`/users/${userId}/websites`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
);
|
||||
const hasData = data && data.length !== 0;
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
{hasData && (
|
||||
<WebsitesTable
|
||||
data={data}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserWebsites;
|
||||
16
src/app/(main)/settings/users/UsersHeader.js
Normal file
16
src/app/(main)/settings/users/UsersHeader.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import UserAddButton from './UserAddButton';
|
||||
|
||||
export function UsersHeader({ onAdd }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.users)}>
|
||||
<UserAddButton onSave={onAdd} />
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
export default UsersHeader;
|
||||
25
src/app/(main)/settings/users/UsersList.js
Normal file
25
src/app/(main)/settings/users/UsersList.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
'use client';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import UsersTable from './UsersTable';
|
||||
import UsersHeader from './UsersHeader';
|
||||
|
||||
export function UsersList() {
|
||||
const { get } = useApi();
|
||||
const filterQuery = useFilterQuery(['users'], params => {
|
||||
return get(`/users`, {
|
||||
...params,
|
||||
});
|
||||
});
|
||||
const { getProps } = filterQuery;
|
||||
|
||||
return (
|
||||
<>
|
||||
<UsersHeader />
|
||||
<DataTable {...getProps()}>{({ data }) => <UsersTable data={data} />}</DataTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UsersList;
|
||||
57
src/app/(main)/settings/users/UsersTable.js
Normal file
57
src/app/(main)/settings/users/UsersTable.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import Link from 'next/link';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import UserDeleteButton from './UserDeleteButton';
|
||||
|
||||
export function UsersTable({ data = [] }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { dateLocale } = useLocale();
|
||||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<GridColumn
|
||||
name="username"
|
||||
label={formatMessage(labels.username)}
|
||||
width={'minmax(200px, 2fr)'}
|
||||
/>
|
||||
<GridColumn name="role" label={formatMessage(labels.role)}>
|
||||
{row =>
|
||||
formatMessage(
|
||||
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown,
|
||||
)
|
||||
}
|
||||
</GridColumn>
|
||||
<GridColumn name="created" label={formatMessage(labels.created)}>
|
||||
{row =>
|
||||
formatDistance(new Date(row.createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
locale: dateLocale,
|
||||
})
|
||||
}
|
||||
</GridColumn>
|
||||
<GridColumn name="action" label=" " alignment="end">
|
||||
{row => {
|
||||
const { id, username } = row;
|
||||
return (
|
||||
<>
|
||||
<Link href={`/settings/users/${id}`}>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
<UserDeleteButton userId={id} username={username} />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default UsersTable;
|
||||
61
src/app/(main)/settings/users/[id]/UserSettings.js
Normal file
61
src/app/(main)/settings/users/[id]/UserSettings.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Item, Loading, Tabs, useToasts } from 'react-basics';
|
||||
import UserEditForm from '../UserEditForm';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import UserWebsites from '../UserWebsites';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function UserSettings({ userId }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [values, setValues] = useState(null);
|
||||
const [tab, setTab] = useState('details');
|
||||
const { get, useQuery } = useApi();
|
||||
const { showToast } = useToasts();
|
||||
const { data, isLoading } = useQuery(
|
||||
['user', userId],
|
||||
() => {
|
||||
if (userId) {
|
||||
return get(`/users/${userId}`);
|
||||
}
|
||||
},
|
||||
{ cacheTime: 0 },
|
||||
);
|
||||
|
||||
const handleSave = data => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
if (data) {
|
||||
setValues(state => ({ ...state, ...data }));
|
||||
}
|
||||
|
||||
if (edit) {
|
||||
setEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setValues(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (isLoading || !values) {
|
||||
return <Loading size="lg" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={values?.username} />
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||
<Item key="websites">{formatMessage(labels.websites)}</Item>
|
||||
</Tabs>
|
||||
{tab === 'details' && <UserEditForm userId={userId} data={values} onSave={handleSave} />}
|
||||
{tab === 'websites' && <UserWebsites userId={userId} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserSettings;
|
||||
9
src/app/(main)/settings/users/[id]/page.js
Normal file
9
src/app/(main)/settings/users/[id]/page.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import UserSettings from './UserSettings';
|
||||
|
||||
export default function ({ params }) {
|
||||
if (process.env.cloudMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <UserSettings userId={params.id} />;
|
||||
}
|
||||
13
src/app/(main)/settings/users/page.tsx
Normal file
13
src/app/(main)/settings/users/page.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import UsersList from 'app/(main)/settings/users/UsersList';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
if (process.env.cloudMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <UsersList />;
|
||||
}
|
||||
export const metadata: Metadata = {
|
||||
title: 'Users | umami',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue