restoring admin dashboard from main

This commit is contained in:
Roland Osborne 2022-08-24 13:30:48 -07:00
parent 9db4c93602
commit 5f69780d81
12 changed files with 800 additions and 36 deletions

View File

@ -1,20 +1,49 @@
import { UserOutlined } from '@ant-design/icons';
import { AdminWrapper } from './Admin.styled';
import React, { useContext, useEffect } from 'react';
import { useNavigate } from "react-router-dom";
import { AppContext } from 'context/AppContext';
import { ViewportContext } from 'context/ViewportContext';
import { useAdmin } from './useAdmin.hook';
import { AdminWrapper } from './Admin.styled';
import { Prompt } from './prompt/Prompt';
import { Dashboard } from './dashboard/Dashboard';
export function Admin() {
import login from 'images/login.png'
export function Admin({ mode }) {
const { state, actions } = useAdmin();
return (
<AdminWrapper>
<div class="app-title">
<span>Databag</span>
<div class="user" onClick={() => actions.onUser()}>
<UserOutlined />
{ (state.display === 'large' || state.display === 'xlarge') && (
<div class="split-layout">
<div class="left">
<img class="splash" src={login} alt="Databag Splash" />
</div>
<div class="right">
{ state.token == null && (
<Prompt login={actions.login} />
)}
{ state.token != null && (
<Dashboard token={state.token} config={state.config} logout={actions.logout} />
)}
</div>
</div>
</div>
)}
{ (state.display === 'medium' || state.display === 'small') && (
<div class="full-layout">
<div class="center">
{ state.token == null && (
<Prompt login={actions.login} />
)}
{ state.token != null && (
<Dashboard token={state.token} config={state.config} logout={actions.logout} />
)}
</div>
</div>
)}
</AdminWrapper>
);
}

View File

@ -2,30 +2,48 @@ import styled from 'styled-components';
import Colors from 'constants/Colors';
export const AdminWrapper = styled.div`
max-width: 400px;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
height: 100%;
.app-title {
font-size: 24px;
.full-layout {
width: 100%;
height: 100%;
padding: 8px;
.center {
width: 100%;
height: 100%;
border-radius: 4px;
background: ${Colors.formBackground};
display: flex;
align-items: center;
justify-content: center;
}
}
.split-layout {
display: flex;
align-items: flex-start;
justify-content: center;
flex: 1;
color: ${Colors.grey};
flex-direction: row;
height: 100%;
.user {
color: ${Colors.grey};
position: absolute;
top: 0px;
right: 0px;
font-size: 20px;
cursor: pointer;
margin: 16px;
.left {
width: 50%;
height: 100%;
padding: 32px;
.splash {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.right {
width: 50%;
height: 100%;
background: ${Colors.formBackground};
display: flex;
align-items: center;
justify-content: center;
}
}
`;

View File

@ -0,0 +1,91 @@
import { DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayout } from './Dashboard.styled';
import { Tooltip, Button, Modal, Input, InputNumber, Space, List } from 'antd';
import { SettingOutlined, CopyOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
import { useDashboard } from './useDashboard.hook';
import { AccountItem } from './accountItem/AccountItem';
export function Dashboard({ token, config, logout }) {
const { state, actions } = useDashboard(token, config);
const onClipboard = (value) => {
navigator.clipboard.writeText(value);
};
const createLink = () => {
return window.location.origin + '/#/create?add=' + state.createToken;
};
return (
<DashboardWrapper>
<div class="container">
<div class="header">
<div class="label">Accounts</div>
<div class="settings">
<Tooltip placement="topRight" title="Reload Accounts">
<SettingsButton type="text" size="small" icon={<ReloadOutlined />}
onClick={() => actions.getAccounts()}></SettingsButton>
</Tooltip>
</div>
<div class="settings">
<Tooltip placement="topRight" title="Configure Server">
<SettingsButton type="text" size="small" icon={<SettingOutlined />}
onClick={() => actions.setShowSettings(true)}></SettingsButton>
</Tooltip>
</div>
<div class="settings">
<Tooltip placement="topRight" title="Logout">
<SettingsButton type="text" size="small" icon={<LogoutOutlined />}
onClick={() => logout()}></SettingsButton>
</Tooltip>
</div>
<div class="add">
<Tooltip placement="topRight" title="Create Account Link">
<AddButton type="text" size="large" icon={<UserAddOutlined />}
loading={state.createBusy} onClick={() => actions.setCreateLink()}></AddButton>
</Tooltip>
</div>
</div>
<div class="body">
<List
locale={{ emptyText: '' }}
itemLayout="horizontal"
dataSource={state.accounts}
loading={state.loading}
renderItem={item => (<AccountItem token={token} item={item}
remove={actions.removeAccount}/>)}
/>
</div>
</div>
<Modal title="Settings" visible={state.showSettings} centered
okText="Save" onOk={() => actions.setSettings()} onCancel={() => actions.setShowSettings(false)}>
<SettingsLayout direction="vertical">
<div class="host">
<div>Federated Host:&nbsp;</div>
<Input placeholder="domain:port/app" onChange={(e) => actions.setHost(e.target.value)}
value={state.host} />
</div>
<div class="storage">
<div>Storage Limit (GB) / Account:&nbsp;</div>
<InputNumber defaultValue={8} onChange={(e) => actions.setStorage(e)}
placeholder="0 for unrestricted" value={state.storage} />
</div>
</SettingsLayout>
</Modal>
<Modal title="Create Account Link" visible={state.showCreate} centered width="fitContent"
footer={[ <Button type="primary" onClick={() => actions.setShowCreate(false)}>OK</Button> ]}
onCancel={() => actions.setShowCreate(false)}>
<CreateLayout>
<div>{createLink()}</div>
<Button icon={<CopyOutlined />} size="small"
onClick={() => onClipboard(createLink())}
/>
</CreateLayout>
</Modal>
</DashboardWrapper>
);
}

View File

@ -0,0 +1,89 @@
import { Button, Space } from 'antd';
import styled from 'styled-components';
export const DashboardWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding-left: 8px;
padding-right: 8px;
.container {
display: flex;
flex-direction: column;
padding-top: 16px;
padding-left: 16px;
padding-right: 16px;
border-radius: 4px;
max-width: 100%;
max-height: 80%;
.header {
color: #444444;
display: flex;
flex-direction: row;
font-size: 20px;
border-bottom: 1px solid #aaaaaa;
}
.body {
padding-top: 8px;
min-height: 0;
overflow: auto;
border-bottom: 1px solid #aaaaaa;
margin-bottom: 16px;
}
.label {
padding-right: 8px;
padding-left: 4px;
display: flex;
align-items: center;
}
.settings {
display: flex;
align-items: center;
}
.add {
display: flex;
align-items: center;
justify-content: flex-end;
flex-grow: 1;
}
}
`;
export const AddButton = styled(Button)`
color: #1890ff;
`;
export const SettingsButton = styled(Button)`
color: #1890ff;
`;
export const SettingsLayout = styled(Space)`
width: 100%;
.host {
white-space: nowrap;
display: flex;
flex-direction: row;
align-items: center;
}
.storage {
white-space: nowrap;
display: flex;
flex-direction: row;
align-items: center;
}
`;
export const CreateLayout = styled(Space)`
white-space: nowrap;
`

View File

@ -0,0 +1,69 @@
import { Logo } from 'logo/Logo';
import { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableButton, ResetButton } from './AccountItem.styled';
import { useAccountItem } from './useAccountItem.hook';
import { CopyOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
import { Modal, Tooltip, Button } from 'antd';
export function AccountItem({ token, item, remove }) {
const { state, actions } = useAccountItem(token, item, remove);
const onClipboard = (value) => {
navigator.clipboard.writeText(value);
};
const Enable = () => {
if (state.disabled) {
return (
<Tooltip placement="topLeft" title="Enable Account">
<EnableButton type="text" size="large" icon={<CheckCircleOutlined />}
loading={state.statusBusy} onClick={() => actions.setStatus(false)}></EnableButton>
</Tooltip>
)
}
return (
<Tooltip placement="topLeft" title="Disable Account">
<DisableButton type="text" size="large" icon={<CloseCircleOutlined />}
loading={state.statusBusy} onClick={() => actions.setStatus(true)}></DisableButton>
</Tooltip>
)
}
const accessLink = () => {
return window.location.origin + '/#/login?access=' + state.accessToken;
};
return (
<AccountItemWrapper>
<div class="avatar">
<Logo url={state.imageUrl} width={32} height={32} radius={4} />
</div>
<div class={state.activeClass}>
<div class="handle">{ state.handle }</div>
<div class="guid">{ state.guid }</div>
</div>
<div class="control">
<Tooltip placement="topLeft" title="Account Login Link">
<ResetButton type="text" size="large" icon={<UnlockOutlined />}
loading={state.accessBusy} onClick={() => actions.setAccessLink()}></ResetButton>
</Tooltip>
<Enable />
<Tooltip placement="topLeft" title="Delete Account">
<DeleteButton type="text" size="large" icon={<UserDeleteOutlined />}
loading={state.removeBusy} onClick={() => actions.remove()}></DeleteButton>
</Tooltip>
</div>
<Modal title="Access Account Link" visible={state.showAccess} centered width="fitContent"
footer={[ <Button type="primary" onClick={() => actions.setShowAccess(false)}>OK</Button> ]}
onCancel={() => actions.setShowAccess(false)}>
<AccessLayout>
<div>{accessLink()}</div>
<Button icon={<CopyOutlined />} size="small"
onClick={() => onClipboard(accessLink())}
/>
</AccessLayout>
</Modal>
</AccountItemWrapper>
);
}

View File

@ -0,0 +1,87 @@
import { Space, Button } from 'antd';
import styled from 'styled-components';
export const AccountItemWrapper = styled.div`
display: flex;
width: 100%;
flex-direction: row;
overflow: hidden;
padding-left: 16px;
padding-right: 16px;
padding-top: 2px;
padding-bottom: 2px;
border-bottom: 1px solid #eeeeee;
align-items: center;
&:hover {
background-color: #eeeeee;
}
.avatar {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
}
.inactive {
padding-left: 16px;
padding-right: 8px;
display: flex;
flex-direction: column;
flex-grow: 1;
color: #cccccc;
}
.active {
padding-left: 16px;
padding-right: 8px;
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
}
.handle {
font-size: 0.8em;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.guid {
font-size: 0.8em;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.control {
flex-grow: 1;
display: flex;
justify-content: flex-end;
align-items: center;
}
`;
export const EnableButton = styled(Button)`
color: orange;
`;
export const DisableButton = styled(Button)`
color: orange;
`;
export const ResetButton = styled(Button)`
color: #1890ff;
`;
export const DeleteButton = styled(Button)`
color: red;
`
export const AccessLayout = styled(Space)`
white-space: nowrap;
`

View File

@ -0,0 +1,78 @@
import { useContext, useState, useEffect } from 'react';
import { getAccountImageUrl } from 'api/getAccountImageUrl';
import { setAccountStatus } from 'api/setAccountStatus';
import { addAccountAccess } from 'api/addAccountAccess';
export function useAccountItem(token, item, remove) {
const [state, setState] = useState({
statusBusy: false,
removeBusy: false,
accessBusy: false,
showAccess: false,
});
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
updateState({
disabled: item?.disabled,
activeClass: item?.disabled ? 'inactive' : 'active',
accountId: item?.accountId,
name: item?.name,
guid: item?.guid,
handle: item?.handle,
imageUrl: item?.imageSet ? getAccountImageUrl(token, item?.accountId) : null,
});
}, [token, item]);
const actions = {
setAccessLink: async () => {
if (!state.accessBusy) {
updateState({ accessBusy: true });
try {
let access = await addAccountAccess(token, item.accountId);
updateState({ accessToken: access, showAccess: true });
}
catch (err) {
window.alert(err);
}
updateState({ accessBusy: false });
}
},
setShowAccess: (showAccess) => {
updateState({ showAccess });
},
remove: async () => {
if (!state.removeBusy) {
updateState({ removeBusy: true });
try {
await remove(state.accountId);
}
catch(err) {
console.log(err);
window.alert(err);
}
updateState({ removeBusy: false });
}
},
setStatus: async (disabled) => {
if (!state.statusBusy) {
updateState({ statusBusy: true });
try {
await setAccountStatus(token, item.accountId, disabled);
updateState({ disabled, activeClass: disabled ? 'inactive' : 'active' });
}
catch(err) {
console.log(err);
window.alert(err);
}
updateState({ statusBusy: false });
}
},
};
return { state, actions };
}

View File

@ -0,0 +1,105 @@
import { useState, useEffect } from 'react';
import { setNodeConfig } from 'api/setNodeConfig';
import { getNodeAccounts } from 'api/getNodeAccounts';
import { removeAccount } from 'api/removeAccount';
import { addAccountCreate } from 'api/addAccountCreate';
export function useDashboard(token, config) {
const [state, setState] = useState({
host: "",
storage: null,
showSettings: false,
busy: false,
loading: false,
accounts: [],
createBusy: false,
showCreate: false,
});
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
setCreateLink: async () => {
if (!state.createBusy) {
updateState({ createBusy: true });
try {
let create = await addAccountCreate(token)
updateState({ createToken: create, showCreate: true });
}
catch (err) {
window.alert(err);
}
updateState({ createBusy: false });
}
},
setShowCreate: (showCreate) => {
updateState({ showCreate });
},
removeAccount: async (accountId) => {
await removeAccount(token, accountId);
actions.getAccounts();
},
setHost: (value) => {
updateState({ host: value });
},
setStorage: (value) => {
updateState({ storage: value });
},
setShowSettings: (value) => {
updateState({ showSettings: value });
},
setSettings: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
await setNodeConfig(token,
{ ...state.config, domain: state.host, accountStorage: state.storage * 1073741824 });
updateState({ showSettings: false });
}
catch(err) {
console.log(err);
window.alert(err);
}
updateState({ busy: false });
}
},
getAccounts: async () => {
if (!state.loading) {
updateState({ loading: true });
try {
let accounts = await getNodeAccounts(token);
accounts.sort((a, b) => {
if (a.handle < b.handle) {
return -1;
}
if (a.handle > b.handle) {
return 1;
}
return 0;
});
updateState({ accounts });
}
catch(err) {
console.log(err);
window.alert(err);
}
updateState({ loading: false });
}
},
};
useEffect(() => {
let storage = config.accountStorage / 1073741824;
if (storage > 1) {
storage = Math.ceil(storage);
}
updateState({ host: config.domain, storage: storage });
actions.getAccounts();
}, []);
return { state, actions };
}

View File

@ -0,0 +1,59 @@
import { Button, Modal, Form, Input } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { PromptWrapper } from './Prompt.styled';
import { usePrompt } from './usePrompt.hook';
export function Prompt({ login }) {
const { state, actions } = usePrompt();
const setLogin = async () => {
try {
let config = await actions.onLogin();
login(state.password, config);
}
catch(err) {
Modal.error({
title: 'Prompt Error',
content: 'Please confirm your admin password.',
});
}
}
const keyDown = (e) => {
if (e.key === 'Enter') {
login()
}
}
return (
<PromptWrapper>
<div class="app-title">
<span>Databag</span>
<div class="user" onClick={() => actions.onUser()}>
<UserOutlined />
</div>
</div>
<div class="form-title">Admin Login</div>
<div class="form-form">
<Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="password">
<Input.Password placeholder="Admin Password" spellCheck="false" onChange={(e) => actions.setPassword(e.target.value)}
autocomplete="current-password" onKeyDown={(e) => keyDown(e)} prefix={<LockOutlined />} size="large" />
</Form.Item>
<div class="form-button">
<div class="form-login">
<Button type="primary" block onClick={setLogin} size="middle" loading={state.busy}>
Login
</Button>
</div>
</div>
</Form>
</div>
</PromptWrapper>
);
};

View File

@ -0,0 +1,58 @@
import styled from 'styled-components';
import Colors from 'constants/Colors';
export const PromptWrapper = styled.div`
max-width: 400px;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
.app-title {
font-size: 24px;
display: flex;
align-items: flex-start;
justify-content: center;
flex: 1;
color: ${Colors.grey};
.user {
color: ${Colors.grey};
position: absolute;
top: 0px;
right: 0px;
font-size: 20px;
cursor: pointer;
margin: 16px;
}
}
.form-title {
font-size: 32px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.form-form {
flex: 2;
.form-button {
display: flex;
align-items: center;
justify-content: center;
.form-login {
width: 50%;
}
}
}
.form-submit {
background-color: #444444;
}
`;

View File

@ -0,0 +1,75 @@
import { useContext, useState, useEffect } from 'react';
import { AppContext } from 'context/AppContext';
import { useNavigate, useLocation } from "react-router-dom";
import { getNodeStatus } from 'api/getNodeStatus';
import { setNodeStatus } from 'api/setNodeStatus';
import { getNodeConfig } from 'api/getNodeConfig';
export function usePrompt() {
const [state, setState] = useState({
password: null,
placeholder: '',
unclaimed: null,
busy: false,
});
const navigate = useNavigate();
const app = useContext(AppContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const checkStatus = async () => {
try {
let status = await getNodeStatus();
updateState({ uncliamed: status });
}
catch(err) {
console.log("failed to check node status");
}
};
useEffect(() => {
checkStatus();
}, []);
const actions = {
setPassword: (password) => {
updateState({ password });
},
onUser: () => {
navigate('/login');
},
onLogin: async () => {
if (!state.busy) {
try {
updateState({ busy: true });
if (state.unclaimed === true) {
await setNodeStatus(state.password);
return await getNodeConfig(state.password);
}
else {
return await getNodeConfig(state.password);
}
updateState({ busy: false });
}
catch (err) {
console.log(err);
updateState({ busy: false });
throw new Error("access denied");
}
}
else {
throw new Error("operation in progress");
}
},
};
useEffect(() => {
}, [app, navigate])
return { state, actions };
}

View File

@ -1,22 +1,28 @@
import { useContext, useState } from 'react';
import { AppContext } from 'context/AppContext';
import { useNavigate } from "react-router-dom";
import { useContext, useState, useEffect } from 'react';
import { ViewportContext } from 'context/ViewportContext';
export function useAdmin() {
const [state, setState] = useState({
display: null,
});
const navigate = useNavigate();
const app = useContext(AppContext);
const viewport = useContext(ViewportContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
updateState({ display: viewport.state.display });
}, [viewport]);
const actions = {
onUser: () => {
navigate('/login');
login: (token, config) => {
updateState({ token, config });
},
logout: () => {
updateState({ token: null, config: null });
},
};