reorganize server dashboard into categories
This commit is contained in:
parent
180f722ed3
commit
b077943e41
4 changed files with 233 additions and 123 deletions
|
@ -5,7 +5,7 @@ import '@revoltchat/ui/src/styles/dark.css';
|
||||||
import '@revoltchat/ui/src/styles/common.css';
|
import '@revoltchat/ui/src/styles/common.css';
|
||||||
import RequireAuth from './components/RequireAuth';
|
import RequireAuth from './components/RequireAuth';
|
||||||
import DashboardHome from './pages/DashboardHome';
|
import DashboardHome from './pages/DashboardHome';
|
||||||
import ServerDashboard from './pages/ServerDashboard';
|
import ServerDashboard from './pages/ServerDashboard/ServerDashboard';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL?.toString()
|
const API_URL = import.meta.env.VITE_API_URL?.toString()
|
||||||
|| 'http://localhost:9000';
|
|| 'http://localhost:9000';
|
||||||
|
|
20
web/src/components/CategorySelector.tsx
Normal file
20
web/src/components/CategorySelector.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { FunctionComponent, useState } from "react";
|
||||||
|
import './styles/CategorySelector.css';
|
||||||
|
|
||||||
|
const CategorySelector: FunctionComponent<{ keys: { id: string, name: string }[], selected: string, onChange: (key: string) => void }> = (props) => {
|
||||||
|
return (
|
||||||
|
<div className="category-selector-outer">
|
||||||
|
{props.keys.map((k) => (
|
||||||
|
<div
|
||||||
|
className={`category-selector-inner ${props.selected == k.id ? 'selected' : ''}`}
|
||||||
|
key={k.id}
|
||||||
|
onClick={() => props.onChange(k.id)}
|
||||||
|
>
|
||||||
|
<span>{k.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategorySelector;
|
40
web/src/components/styles/CategorySelector.css
Normal file
40
web/src/components/styles/CategorySelector.css
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
.category-selector-outer {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
margin: 8px 10px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: var(--secondary-background);
|
||||||
|
display: flex;
|
||||||
|
border-radius: 6px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-selector-inner {
|
||||||
|
background-color: var(--tertiary-background);
|
||||||
|
height: 24px;
|
||||||
|
margin: 4px;
|
||||||
|
width: 100vw;
|
||||||
|
user-select: none;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: filter .2s, background-color .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-selector-inner:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-selector-inner:active {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-selector-inner.selected {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-selector-inner span {
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
}
|
||||||
|
.category-selector-inner.selected span {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
|
@ -10,10 +10,11 @@ import { H3 } from '@revoltchat/ui/lib/components/atoms/heading/H3';
|
||||||
import { H4 } from '@revoltchat/ui/lib/components/atoms/heading/H4';
|
import { H4 } from '@revoltchat/ui/lib/components/atoms/heading/H4';
|
||||||
import { Icon } from '@mdi/react';
|
import { Icon } from '@mdi/react';
|
||||||
import { mdiChevronLeft, mdiCloseBox } from '@mdi/js';
|
import { mdiChevronLeft, mdiCloseBox } from '@mdi/js';
|
||||||
import { API_URL } from "../App";
|
import { API_URL } from "../../App";
|
||||||
import { getAuthHeaders } from "../utils";
|
import { getAuthHeaders } from "../../utils";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import defaultChannelIcon from '../assets/channel-default-icon.svg';
|
import defaultChannelIcon from '../../assets/channel-default-icon.svg';
|
||||||
|
import CategorySelector from '../../components/CategorySelector';
|
||||||
|
|
||||||
type User = { id: string, username?: string, avatarURL?: string }
|
type User = { id: string, username?: string, avatarURL?: string }
|
||||||
type Channel = { id: string, name: string, icon?: string, type: 'VOICE'|'TEXT', nsfw: boolean }
|
type Channel = { id: string, name: string, icon?: string, type: 'VOICE'|'TEXT', nsfw: boolean }
|
||||||
|
@ -40,6 +41,8 @@ type AntispamRule = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerDashboard: FunctionComponent = () => {
|
const ServerDashboard: FunctionComponent = () => {
|
||||||
|
const [category, setCategory] = useState('home');
|
||||||
|
|
||||||
const [serverInfo, setServerInfo] = useState({} as Server);
|
const [serverInfo, setServerInfo] = useState({} as Server);
|
||||||
const [status, setStatus] = useState('');
|
const [status, setStatus] = useState('');
|
||||||
|
|
||||||
|
@ -77,7 +80,7 @@ const ServerDashboard: FunctionComponent = () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${API_URL}/dash/server/${serverid}`, { headers: await getAuthHeaders() });
|
const res = await axios.get(`${API_URL}/dash/server/${serverid}`, { headers: await getAuthHeaders() });
|
||||||
console.log(res.data);
|
console.log(res.data);
|
||||||
|
|
||||||
const server: Server = res.data.server;
|
const server: Server = res.data.server;
|
||||||
setServerInfo(server);
|
setServerInfo(server);
|
||||||
|
|
||||||
|
@ -108,131 +111,179 @@ const ServerDashboard: FunctionComponent = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link to='/dashboard'>
|
{status.length ? <a>{status}</a> : <></>}
|
||||||
<div style={{ display: 'flex', marginTop: '4px' }}>
|
<div
|
||||||
<Icon path={mdiChevronLeft} style={{ height: '24px' }} />
|
style={{
|
||||||
<span>Back</span>
|
marginTop: '8px',
|
||||||
</div>
|
marginLeft: '8px',
|
||||||
</Link>
|
textOverflow: 'ellipsis',
|
||||||
<H1 style={{ marginTop: '8px' }}>{serverInfo?.name ?? 'Loading...'}</H1>
|
whiteSpace: 'nowrap',
|
||||||
{status.length ? <a>{status}</a> : <br/>}
|
overflow: 'hidden',
|
||||||
|
color: 'var(--secondary-foreground)',
|
||||||
|
maxWidth: 'calc(100% - 20px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link to='/dashboard' style={{ float: 'left' }}>
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<Icon path={mdiChevronLeft} style={{ height: '25px' }} />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
marginLeft: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{serverInfo?.name ?? 'Loading...'}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: 'var(--secondary-foreground)', marginLeft: '6px' }}>
|
||||||
|
•
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: 'var(--secondary-foreground)',
|
||||||
|
marginLeft: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{serverInfo.description || <i>No server description set</i>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CategorySelector
|
||||||
|
keys={[
|
||||||
|
{ id: 'home', name: 'Home' },
|
||||||
|
{ id: 'automod', name: 'Moderation Rules' },
|
||||||
|
]}
|
||||||
|
selected={category}
|
||||||
|
onChange={setCategory}
|
||||||
|
/>
|
||||||
|
|
||||||
<div hidden={Object.keys(serverInfo).length == 0}>
|
<div hidden={Object.keys(serverInfo).length == 0}>
|
||||||
<H4>{serverInfo.description ?? <i>No server description set</i>}</H4>
|
|
||||||
<br/>
|
|
||||||
<div style={{ paddingLeft: '10px', paddingRight: '10px' }}>
|
<div style={{ paddingLeft: '10px', paddingRight: '10px' }}>
|
||||||
<>
|
|
||||||
<H3>Prefix</H3>
|
|
||||||
<InputBox
|
|
||||||
style={{ width: '150px', }}
|
|
||||||
placeholder="Enter a prefix..."
|
|
||||||
value={prefix}
|
|
||||||
onChange={e => {
|
|
||||||
setPrefix(e.currentTarget.value);
|
|
||||||
setChanged({ ...changed, prefix: true });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
style={{ maxWidth: '400px' }}
|
|
||||||
value={prefixAllowSpace}
|
|
||||||
onChange={() => {
|
|
||||||
setPrefixAllowSpace(!prefixAllowSpace);
|
|
||||||
setChanged({ ...changed, prefixAllowSpace: true });
|
|
||||||
}}
|
|
||||||
title="Allow space after prefix"
|
|
||||||
description={'Whether the bot recognizes a command if the prefix is followed by a space. Enable if your prefix is a word.'}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
style={{ marginTop: "16px" }}
|
|
||||||
onClick={saveConfig}
|
|
||||||
>Save</Button>
|
|
||||||
</>
|
|
||||||
|
|
||||||
<LineDivider />
|
{category == 'home' && (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
<H3>Prefix</H3>
|
||||||
|
<InputBox
|
||||||
|
style={{ width: '150px', }}
|
||||||
|
placeholder="Enter a prefix..."
|
||||||
|
value={prefix}
|
||||||
|
onChange={e => {
|
||||||
|
setPrefix(e.currentTarget.value);
|
||||||
|
setChanged({ ...changed, prefix: true });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
style={{ maxWidth: '400px' }}
|
||||||
|
value={prefixAllowSpace}
|
||||||
|
onChange={() => {
|
||||||
|
setPrefixAllowSpace(!prefixAllowSpace);
|
||||||
|
setChanged({ ...changed, prefixAllowSpace: true });
|
||||||
|
}}
|
||||||
|
title="Allow space after prefix"
|
||||||
|
description={'Whether the bot recognizes a command if the prefix is followed by a space. Enable if your prefix is a word.'}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style={{ marginTop: "16px" }}
|
||||||
|
onClick={saveConfig}
|
||||||
|
>Save</Button>
|
||||||
|
</>
|
||||||
|
|
||||||
<>
|
<LineDivider />
|
||||||
<H3>Bot Managers</H3>
|
|
||||||
<H4>
|
|
||||||
Only users with "Manage Server" permission are allowed to add/remove other
|
|
||||||
bot managers and are automatically considered bot manager.
|
|
||||||
</H4>
|
|
||||||
<UserListTypeContainer>
|
|
||||||
<UserListContainer disabled={(serverInfo.perms ?? 0) < 3}>
|
|
||||||
{botManagers.map((uid: string) => {
|
|
||||||
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
|
||||||
return (
|
|
||||||
<UserListEntry type='MANAGER' user={user} key={uid} />
|
|
||||||
)})}
|
|
||||||
<UserListAddField type='MANAGER' />
|
|
||||||
</UserListContainer>
|
|
||||||
</UserListTypeContainer>
|
|
||||||
|
|
||||||
<H3>Moderators</H3>
|
<>
|
||||||
<H4>
|
<H3>Bot Managers</H3>
|
||||||
Only bot managers are allowed to add/remove moderators.
|
<H4>
|
||||||
All bot managers are also moderators.
|
Only users with "Manage Server" permission are allowed to add/remove other
|
||||||
</H4>
|
bot managers and are automatically considered bot manager.
|
||||||
<UserListTypeContainer>
|
</H4>
|
||||||
<UserListContainer disabled={(serverInfo.perms ?? 0) < 2}>
|
<UserListTypeContainer>
|
||||||
{moderators.map((uid: string) => {
|
<UserListContainer disabled={(serverInfo.perms ?? 0) < 3}>
|
||||||
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
{botManagers.map((uid: string) => {
|
||||||
return (
|
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
||||||
<UserListEntry type='MOD' user={user} key={uid} />
|
return (
|
||||||
)})}
|
<UserListEntry type='MANAGER' user={user} key={uid} />
|
||||||
<UserListAddField type='MOD' />
|
)})}
|
||||||
</UserListContainer>
|
<UserListAddField type='MANAGER' />
|
||||||
</UserListTypeContainer>
|
</UserListContainer>
|
||||||
</>
|
</UserListTypeContainer>
|
||||||
|
|
||||||
<LineDivider />
|
<H3>Moderators</H3>
|
||||||
|
<H4>
|
||||||
|
Only bot managers are allowed to add/remove moderators.
|
||||||
|
All bot managers are also moderators.
|
||||||
|
</H4>
|
||||||
|
<UserListTypeContainer>
|
||||||
|
<UserListContainer disabled={(serverInfo.perms ?? 0) < 2}>
|
||||||
|
{moderators.map((uid: string) => {
|
||||||
|
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
||||||
|
return (
|
||||||
|
<UserListEntry type='MOD' user={user} key={uid} />
|
||||||
|
)})}
|
||||||
|
<UserListAddField type='MOD' />
|
||||||
|
</UserListContainer>
|
||||||
|
</UserListTypeContainer>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<>
|
{category == 'automod' && (
|
||||||
<H3>Antispam Rules</H3>
|
<>
|
||||||
{serverInfo.perms != null && automodSettings && (
|
<H3>Antispam Rules</H3>
|
||||||
serverInfo.perms > 0
|
{serverInfo.perms != null && automodSettings && (
|
||||||
? (
|
serverInfo.perms > 0
|
||||||
<>
|
? (
|
||||||
{automodSettings.antispam.map(r => <AntispamRule rule={r} key={r.id} />)}
|
<>
|
||||||
<Button style={{
|
{automodSettings.antispam.map((r, i) => (
|
||||||
marginTop: '12px',
|
<>
|
||||||
marginBottom: '8px',
|
<AntispamRule rule={r} key={r.id} />
|
||||||
}} onClick={async () => {
|
{i < automodSettings.antispam.length - 1 && <LineDivider/>}
|
||||||
const newRule: AntispamRule = {
|
</>
|
||||||
action: 0,
|
))}
|
||||||
max_msg: 5,
|
<Button style={{
|
||||||
timeframe: 3,
|
marginTop: '12px',
|
||||||
message: null,
|
marginBottom: '8px',
|
||||||
id: '',
|
}} onClick={async () => {
|
||||||
channels: [],
|
const newRule: AntispamRule = {
|
||||||
}
|
action: 0,
|
||||||
|
max_msg: 5,
|
||||||
|
timeframe: 3,
|
||||||
|
message: null,
|
||||||
|
id: '',
|
||||||
|
channels: [],
|
||||||
|
}
|
||||||
|
|
||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
`${API_URL}/dash/server/${serverid}/automod`,
|
`${API_URL}/dash/server/${serverid}/automod`,
|
||||||
{
|
{
|
||||||
action: newRule.action,
|
action: newRule.action,
|
||||||
max_msg: newRule.max_msg,
|
max_msg: newRule.max_msg,
|
||||||
timeframe: newRule.timeframe,
|
timeframe: newRule.timeframe,
|
||||||
},
|
},
|
||||||
{ headers: await getAuthHeaders() }
|
{ headers: await getAuthHeaders() }
|
||||||
);
|
);
|
||||||
|
|
||||||
newRule.id = res.data.id;
|
newRule.id = res.data.id;
|
||||||
|
|
||||||
setAutomodSettings({ antispam: [ ...(automodSettings.antispam), newRule ] });
|
setAutomodSettings({ antispam: [ ...(automodSettings.antispam), newRule ] });
|
||||||
}}>
|
}}>
|
||||||
Create Rule
|
Create Rule
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div>
|
||||||
|
<p style={{ color: 'var(--foreground)' }}>
|
||||||
|
You do not have access to this.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
: (
|
}
|
||||||
<div>
|
</>
|
||||||
<p style={{ color: 'var(--foreground)' }}>
|
)}
|
||||||
You do not have access to this.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -296,7 +347,7 @@ const ServerDashboard: FunctionComponent = () => {
|
||||||
`${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}/${props.user.id}`,
|
`${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}/${props.user.id}`,
|
||||||
{ headers: await getAuthHeaders() }
|
{ headers: await getAuthHeaders() }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (props.type == 'MANAGER') {
|
if (props.type == 'MANAGER') {
|
||||||
setBotManagers(res.data.managers);
|
setBotManagers(res.data.managers);
|
||||||
}
|
}
|
||||||
|
@ -308,7 +359,7 @@ const ServerDashboard: FunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserListContainer(props: { disabled: boolean, children: any }) {
|
function UserListContainer(props: { disabled: boolean, children: any }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -406,7 +457,7 @@ const ServerDashboard: FunctionComponent = () => {
|
||||||
.find(c => c.name == content)
|
.find(c => c.name == content)
|
||||||
|| serverInfo.channels // Prefer channel with same capitalization,
|
|| serverInfo.channels // Prefer channel with same capitalization,
|
||||||
.find(c => c.name.toLowerCase() == content.toLowerCase()); // otherwise search case insensitive
|
.find(c => c.name.toLowerCase() == content.toLowerCase()); // otherwise search case insensitive
|
||||||
|
|
||||||
if (channel && channel.type == 'TEXT') {
|
if (channel && channel.type == 'TEXT') {
|
||||||
props.onInput(channel);
|
props.onInput(channel);
|
||||||
setContent('');
|
setContent('');
|
||||||
|
@ -613,7 +664,6 @@ const ServerDashboard: FunctionComponent = () => {
|
||||||
{props.rule.id}
|
{props.rule.id}
|
||||||
</code>
|
</code>
|
||||||
<div style={{ clear: 'both' }} />
|
<div style={{ clear: 'both' }} />
|
||||||
<div style={{ maxWidth: 'max(40%, 600px)' }}><LineDivider/></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
Loading…
Reference in a new issue