mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-25 08:12:19 +01:00
refactored user-view
This commit is contained in:
@@ -37,7 +37,7 @@ export default function AppUser(props: AppUserProps) {
|
||||
<div className="user-app">
|
||||
<div className={'user-app__header'}>
|
||||
<div className={'user-app__header__caption'}><span className={'user-app__header__logo'}>{appLogo}</span>{appTitle}</div>
|
||||
<div className={'app-header__toolbar'}><select onChange={handleLanguage}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l} selected={l === i18next.language}>{l}</option>)}</select></div>
|
||||
<div className={'app-header__toolbar'}><select onChange={handleLanguage} defaultValue={i18next.language}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l}>{l}</option>)}</select></div>
|
||||
<div className={'user-app__header__toolbar'}><button title={translate('LABEL.LOGOUT')} onClick={handleLogout}>{getIconByName('Logout')}</button></div>
|
||||
</div>
|
||||
<div className={'user-app__main'}>
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function App(props: AppProps) {
|
||||
<div className="app">
|
||||
<div className={'app-header'}>
|
||||
<div className={'app-header__caption'}><span className={'app-header__logo'}>{appLogo}</span>{appTitle}</div>
|
||||
<div className={'app-header__toolbar'}><select onChange={handleLanguage}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l} selected={l === i18next.language}>{l}</option>)}</select></div>
|
||||
<div className={'app-header__toolbar'}><select onChange={handleLanguage} defaultValue={i18next.language}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l}>{l}</option>)}</select></div>
|
||||
<div className={'app-header__toolbar'}><button title={translate('LABEL.CONFIGURATION')} onClick={handlePreferences}>{getIconByName('Config')}</button></div>
|
||||
<div className={'app-header__toolbar'}><button title={translate('LABEL.LOGOUT')} onClick={handleLogout}>{getIconByName('Logout')}</button></div>
|
||||
</div>
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
flex: 1 1 auto;
|
||||
font-size: inherit;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex: 1 1 auto;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: inherit;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
@@ -25,32 +27,32 @@
|
||||
position: absolute;
|
||||
color: var(--label-color);
|
||||
transform: translate(6px, -0.8rem);
|
||||
background-color: var(--background-color);
|
||||
padding: 0 2px;
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0 6px;
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
input {
|
||||
color: var(--text-color);
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
border: none !important;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
height: 100%;
|
||||
font-size: 1.1rem !important;
|
||||
outline: none !important;
|
||||
padding: 0 4px 0 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
@include common.iconButton();
|
||||
border-radius: 0;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
&:last-child {
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--card-background-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
.playlist-filter {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 10px;
|
||||
padding: 10px 0 10px 0;
|
||||
max-height: 2.5rem;
|
||||
font-size: 0.5rem;
|
||||
|
||||
&__option-active {
|
||||
box-sizing: border-box !important;
|
||||
transform: scale(0.8);
|
||||
transform: scale(0.9);
|
||||
background-color: var(--option-active-background-color) !important;
|
||||
color: var(--option-active-color) !important;
|
||||
svg {
|
||||
@@ -13,7 +15,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__error-message {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useRef, KeyboardEvent, useState} from "react";
|
||||
import React, {KeyboardEvent, useCallback, useEffect, useRef, useState} from "react";
|
||||
import './playlist-filter.scss';
|
||||
import {getIconByName} from "../../icons/icons";
|
||||
import InputField from "../input-field/input-field";
|
||||
@@ -14,16 +14,24 @@ function isValidRegExp(pattern: string): boolean {
|
||||
}
|
||||
|
||||
interface PlaylistFilterProps {
|
||||
options?: { filter: string, regexp: boolean }
|
||||
onFilter: (filter: string, regexp: boolean) => void;
|
||||
}
|
||||
|
||||
export default function PlaylistFilter(props: PlaylistFilterProps) {
|
||||
const {onFilter} = props;
|
||||
const {onFilter, options} = props;
|
||||
const translate = useTranslator();
|
||||
const textField = useRef<HTMLInputElement>(undefined);
|
||||
const [useRegexp, setUseRegexp] = useState<boolean>(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(()=> {
|
||||
setUseRegexp(options?.regexp ?? false);
|
||||
if (textField.current) {
|
||||
textField.current.value = options?.filter ?? '';
|
||||
}
|
||||
}, [options?.regexp, options?.filter]);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
const value = textField.current.value;
|
||||
setErrorMsg(undefined);
|
||||
@@ -36,7 +44,7 @@ export default function PlaylistFilter(props: PlaylistFilterProps) {
|
||||
} else {
|
||||
onFilter(value, useRegexp);
|
||||
}
|
||||
}, [onFilter,useRegexp, translate]);
|
||||
}, [onFilter, useRegexp, translate]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
textField.current.value = '';
|
||||
@@ -56,7 +64,8 @@ export default function PlaylistFilter(props: PlaylistFilterProps) {
|
||||
return <div className={'playlist-filter'}>
|
||||
<InputField label={translate('LABEL.SEARCH')}>
|
||||
<input type="text" ref={textField} onKeyUp={handleKeyPress}/>
|
||||
<button title={translate('LABEL.REGEXP')} className={useRegexp ? 'playlist-filter__option-active' : ''} onClick={handleRegexp}>{getIconByName('Regexp')}</button>
|
||||
<button title={translate('LABEL.REGEXP')} className={useRegexp ? 'playlist-filter__option-active' : ''}
|
||||
onClick={handleRegexp}>{getIconByName('Regexp')}</button>
|
||||
<button title={translate('LABEL.CLEAR')} onClick={handleClear}>{getIconByName('ClearSearch')}</button>
|
||||
<button title={translate('LABEL.SEARCH')} onClick={handleSearch}>{getIconByName('Search')}</button>
|
||||
</InputField>
|
||||
|
||||
@@ -7,7 +7,7 @@ interface SvgIconProps {
|
||||
}
|
||||
|
||||
function SvgIcon(props: SvgIconProps): React.JSX.Element {
|
||||
return <svg className="svg-icon" focusable="false" aria-hidden="true" data-testid={props.name} viewBox='0 0 24 24'>
|
||||
return <svg className="svg-icon" focusable="false" aria-hidden="true" fill="inherit" data-testid={props.name} viewBox='0 0 24 24'>
|
||||
<path d={props.path}/>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
@@ -131,16 +131,20 @@ export default function UserPlaylist(props: UserPlaylistProps) {
|
||||
}, [activeTab, getActiveCategories]);
|
||||
|
||||
const handleFilter = useCallback((filter: string, regexp: boolean): void => {
|
||||
let filer_value = regexp ? filter : filter.toLowerCase();
|
||||
const filtered = (categories as any)?.[activeTab]?.filter((cat: PlaylistCategory) => {
|
||||
if (regexp) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return cat.name.trim().match(filer_value) != undefined;
|
||||
} else {
|
||||
return (cat.name.trim().toLowerCase().indexOf(filer_value) > -1);
|
||||
}
|
||||
}) ?? [];
|
||||
setFilteredCategories(filteredCategories => ({...filteredCategories, [activeTab]: filtered}));
|
||||
let filter_value = regexp ? filter : filter.toLowerCase();
|
||||
if (filter_value?.length) {
|
||||
const filtered = (categories as any)?.[activeTab]?.filter((cat: PlaylistCategory) => {
|
||||
if (regexp) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return cat.name.trim().match(filter_value) != undefined;
|
||||
} else {
|
||||
return (cat.name.trim().toLowerCase().indexOf(filter_value) > -1);
|
||||
}
|
||||
}) ?? [];
|
||||
setFilteredCategories(filteredCategories => ({...filteredCategories, [activeTab]: filtered}));
|
||||
} else {
|
||||
setFilteredCategories(filteredCategories => ({...filteredCategories, [activeTab]: (categories as any)?.[activeTab]}));
|
||||
}
|
||||
}, [activeTab, categories]);
|
||||
|
||||
const handleShowSelected = useCallback((event: any) => {
|
||||
@@ -156,7 +160,7 @@ export default function UserPlaylist(props: UserPlaylistProps) {
|
||||
<button title={translate('LABEL.SAVE')} onClick={handleSave}>{translate('LABEL.SAVE')}</button>
|
||||
</div>
|
||||
<TabSet tabs={tabs} active={activeTab} onTabChange={handleTabChange}></TabSet>
|
||||
{CATEGORY_TABS.map(tab => <div className={'user-playlist__categories-panel' + (activeTab !== tab.key ? ' hidden' : '')}>
|
||||
{CATEGORY_TABS.map(tab => <div key={tab.key} className={'user-playlist__categories-panel' + (activeTab !== tab.key ? ' hidden' : '')}>
|
||||
<div className={'user-playlist__categories__toolbar'}>
|
||||
<div className={'user-playlist__categories__toolbar-filter'}>
|
||||
<PlaylistFilter onFilter={handleFilter}></PlaylistFilter>
|
||||
|
||||
@@ -114,21 +114,25 @@
|
||||
vertical-align: middle;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
input {
|
||||
border: none !important;
|
||||
height: 2rem;
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
&-number {
|
||||
max-width: 50px;
|
||||
&-value {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&-date {
|
||||
width: 120px;
|
||||
.visibility {
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--icon-button-hover-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&-toolbar {
|
||||
& {
|
||||
display: flex;
|
||||
@@ -173,22 +177,16 @@
|
||||
}
|
||||
|
||||
&-col {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
|
||||
input {
|
||||
border-top: 1px solid var(--table-border-color) !important;
|
||||
border-bottom: 1px solid var(--table-border-color) !important;
|
||||
&-toolbar {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
input {
|
||||
@include preferences.prefsInput;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import './user-view.scss';
|
||||
import ServerConfig, {TargetUser} from "../../model/server-config";
|
||||
import ServerConfig, {Credentials, TargetUser} from "../../model/server-config";
|
||||
import {getIconByName} from "../../icons/icons";
|
||||
import TextGenerator from "../../utils/text-generator";
|
||||
import {useSnackbar} from "notistack";
|
||||
import {useServices} from "../../provider/service-provider";
|
||||
import ConfigUtils from "../../utils/config-utils";
|
||||
import TabSet, {TabSetTab} from "../tab-set/tab-set";
|
||||
import TagSelect from "../tag-select/tags-select";
|
||||
import DatePicker from "react-date-picker";
|
||||
import useTranslator from "../../hook/use-translator";
|
||||
import DateUtils from "../../utils/date-utils";
|
||||
import PlaylistFilter from "../playlist-filter/playlist-filter";
|
||||
|
||||
const PROXY_OPTIONS = [
|
||||
{value: 'reverse', label: 'Reverse'},
|
||||
{value: 'redirect', label: 'Redirect'}
|
||||
];
|
||||
const STATUS_OPTIONS = [
|
||||
{value: 'Active', label: 'Active'},
|
||||
{value: 'Expired', label: 'Expired'},
|
||||
{value: 'Banned', label: 'Banned'},
|
||||
{value: 'Trial', label: 'Trial'},
|
||||
{value: 'Disabled', label: 'Disabled'},
|
||||
{value: 'Pending', label: 'Pending'},
|
||||
];
|
||||
const COLUMNS = [
|
||||
{field: 'username', label: 'LABEL.USERNAME'},
|
||||
{field: 'password', label: 'LABEL.PASSWORD', hidden: true},
|
||||
{field: 'token', label: 'LABEL.TOKEN', hidden: true},
|
||||
{field: 'server', label: 'LABEL.SERVER'},
|
||||
{field: 'proxy', label: 'LABEL.PROXY'},
|
||||
{field: 'max_connections', label: 'LABEL.MAX_CON'},
|
||||
{field: 'status', label: 'LABEL.STATUS'},
|
||||
{field: 'exp_date', label: 'LABEL.EXP_DATE', render: (value: any, hidden?: boolean) => DateUtils.formatDate(value)},
|
||||
]
|
||||
|
||||
COLUMNS.forEach(col => {
|
||||
if (!col.render) {
|
||||
col.render = (value: any, hidden?: boolean) => value ? (hidden ? '***' : '' + value) : '';
|
||||
}
|
||||
})
|
||||
|
||||
const prepareCredentials = (targetUser: TargetUser[]) => {
|
||||
targetUser.forEach((user) => {
|
||||
@@ -55,12 +58,16 @@ export default function UserView(props: UserViewProps) {
|
||||
const services = useServices();
|
||||
const translate = useTranslator();
|
||||
const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar();
|
||||
const [serverOptions, setServerOptions] = useState<{ value: string, label: string }[]>([]);
|
||||
const [targets, setTargets] = useState<TargetUser[]>([]);
|
||||
const [activeTarget, setActiveTarget] = useState<string>(undefined);
|
||||
const [tabs, setTabs] = useState<TabSetTab[]>([]);
|
||||
const [serverOptions, setServerOptions] = useState<{ value: string, label: string }[]>([]);
|
||||
const proxy_options = useMemo(() => PROXY_OPTIONS.map(c => ({...c, label: translate(c.label)})), [translate]);
|
||||
const status_options = useMemo(() => STATUS_OPTIONS.map(c => ({...c, label: translate(c.label)})), [translate]);
|
||||
const [showHiddenFields, setShowHiddenFields] = useState<Record<string, boolean>>({});
|
||||
const [filteredUser, setFilteredUser] = useState<Record<string, {
|
||||
filter: string,
|
||||
regexp: boolean,
|
||||
user: Credentials[]
|
||||
}>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
@@ -80,92 +87,82 @@ export default function UserView(props: UserViewProps) {
|
||||
missing?.forEach(target => {
|
||||
result.push({src: false, target: target.target, credentials: target.credentials} as any);
|
||||
});
|
||||
const targets = result || [];
|
||||
const targets: TargetUser [] = result || [];
|
||||
setTargets(targets);
|
||||
if (targets?.length) {
|
||||
setActiveTarget(targets[0].target);
|
||||
const hiddenFields = COLUMNS.filter(c => c.hidden);
|
||||
const hidden: any = {};
|
||||
targets.forEach(target => target.credentials.forEach(c => {
|
||||
hiddenFields.forEach(h => {
|
||||
hidden[c.username + h.field] = true
|
||||
});
|
||||
}));
|
||||
setShowHiddenFields(hidden);
|
||||
}
|
||||
setTabs(targets.map(target => ({key: target.target, label: target.target})));
|
||||
}
|
||||
}, [config])
|
||||
|
||||
const handleUserAdd = useCallback((evt: any) => {
|
||||
const target_name = evt.target.dataset.target;
|
||||
const target = targets.find(target => target.target === target_name);
|
||||
if (target) {
|
||||
const usernameExists = (uname: string): boolean => {
|
||||
for (const target of targets) {
|
||||
if (target.credentials.find(c => c.username === uname)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
let cnt = 0;
|
||||
let username = TextGenerator.generateUsername().toLowerCase();
|
||||
while (usernameExists(username)) {
|
||||
username = TextGenerator.generateUsername().toLowerCase();
|
||||
cnt++;
|
||||
if (cnt > 1000) {
|
||||
username = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
target.credentials.push({
|
||||
username,
|
||||
password: TextGenerator.generatePassword(),
|
||||
token: TextGenerator.generatePassword(),
|
||||
proxy: 'reverse',
|
||||
created_at,
|
||||
exp_date: undefined,
|
||||
max_connections: undefined,
|
||||
status: "Active",
|
||||
});
|
||||
setTargets([...targets]);
|
||||
}
|
||||
}, [targets]);
|
||||
// const target_name = evt.target.dataset.target;
|
||||
// const target = targets.find(target => target.target === target_name);
|
||||
// if (target) {
|
||||
// const usernameExists = (uname: string): boolean => {
|
||||
// for (const target of targets) {
|
||||
// if (target.credentials.find(c => c.username === uname)) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
// };
|
||||
// let cnt = 0;
|
||||
// let username = TextGenerator.generateUsername().toLowerCase();
|
||||
// while (usernameExists(username)) {
|
||||
// username = TextGenerator.generateUsername().toLowerCase();
|
||||
// cnt++;
|
||||
// if (cnt > 1000) {
|
||||
// username = "";
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// const created_at = Math.floor(Date.now() / 1000);
|
||||
// target.credentials.push({
|
||||
// username,
|
||||
// password: TextGenerator.generatePassword(),
|
||||
// token: TextGenerator.generatePassword(),
|
||||
// proxy: 'reverse',
|
||||
// created_at,
|
||||
// exp_date: undefined,
|
||||
// max_connections: undefined,
|
||||
// status: "Active",
|
||||
// });
|
||||
// setTargets([...targets]);
|
||||
// }
|
||||
}, []);
|
||||
|
||||
const handleUserRemove = useCallback((evt: any) => {
|
||||
const idx = evt.target.dataset.idx;
|
||||
const username = evt.target.dataset.user;
|
||||
const target_name = evt.target.dataset.target;
|
||||
const target = targets.find(target => target.target === target_name);
|
||||
if (target) {
|
||||
target.credentials.splice(idx, 1);
|
||||
setTargets([...targets]);
|
||||
let idx = target.credentials.findIndex(c => c.username === username)
|
||||
if (idx >= 0) {
|
||||
target.credentials.splice(idx, 1);
|
||||
setTargets([...targets]);
|
||||
}
|
||||
}
|
||||
}, [targets]);
|
||||
|
||||
const handleValueChange = useCallback((evt: any) => {
|
||||
const handleUserEdit = useCallback((evt: any) => {
|
||||
const username = evt.target.dataset.user;
|
||||
const target_name = evt.target.dataset.target;
|
||||
const target = targets.find(target => target.target === target_name);
|
||||
if (target) {
|
||||
const idx = evt.target.dataset.idx;
|
||||
const field: any = evt.target.dataset.field;
|
||||
(target.credentials[idx] as any)[field] = evt.target.value;
|
||||
setTargets([...targets]); // this is needed for DatePicker for change detection
|
||||
}
|
||||
}, [targets]);
|
||||
const user = target.credentials.find(c => c.username === username);
|
||||
if (user) {
|
||||
|
||||
const handleChange = useCallback((fieldWithTargetAndIndex: string, value: any) => {
|
||||
const parts = fieldWithTargetAndIndex.split('@');
|
||||
const target_name = parts[0];
|
||||
const target = targets.find(target => target.target === target_name);
|
||||
if (target) {
|
||||
const idx = parseInt(parts[1]);
|
||||
const credentials: any = target.credentials[idx];
|
||||
credentials[parts[2]] = value;
|
||||
}
|
||||
}, [targets]);
|
||||
|
||||
const handleSelectChange = useCallback((event: any) => {
|
||||
const parts = event.target.name.split('@');
|
||||
const target_name = parts[0];
|
||||
const target = targets.find(target => target.target === target_name);
|
||||
if (target) {
|
||||
const idx = parseInt(parts[1]);
|
||||
const credentials: any = target.credentials[idx];
|
||||
credentials[parts[2]] = event.target.value;
|
||||
}
|
||||
}
|
||||
}, [targets]);
|
||||
|
||||
@@ -215,13 +212,47 @@ export default function UserView(props: UserViewProps) {
|
||||
});
|
||||
}, [targets, services, enqueueSnackbar, translate]);
|
||||
|
||||
const handleVisibility = useCallback((evt: any) => {
|
||||
let key = evt.target.dataset.hiddenkey;
|
||||
setShowHiddenFields((hiddenFields: any) => ({...hiddenFields, [key]: !hiddenFields[key]}));
|
||||
}, []);
|
||||
|
||||
const handleTabChange = useCallback((target: string) => {
|
||||
|
||||
setActiveTarget(target);
|
||||
}, []);
|
||||
|
||||
const handleFilter = useCallback((filter: string, regexp: boolean): void => {
|
||||
let filter_value = regexp ? filter : filter.toLowerCase();
|
||||
const target = targets.find(t => t.target === activeTarget);
|
||||
|
||||
if (target) {
|
||||
if (filter_value?.length) {
|
||||
const filtered = target.credentials.filter((credential: Credentials) => {
|
||||
if (regexp) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return credential.username.trim().match(filter_value) != undefined;
|
||||
} else {
|
||||
return (credential.username.trim().toLowerCase().indexOf(filter_value) > -1);
|
||||
}
|
||||
}) ?? [];
|
||||
setFilteredUser((filteredUser: any) => ({
|
||||
...filteredUser,
|
||||
[activeTarget]: {filter, regexp, user: filtered}
|
||||
}));
|
||||
} else {
|
||||
setFilteredUser((filteredUser: any) => ({
|
||||
...filteredUser, [activeTarget]: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
}, [activeTarget, targets]);
|
||||
|
||||
return <div className={'user'}>
|
||||
|
||||
<div className={'user__toolbar'}><label>{translate('LABEL.USER')}</label>
|
||||
<PlaylistFilter onFilter={handleFilter} options={filteredUser[activeTarget]}></PlaylistFilter>
|
||||
<button title={'Save'} onClick={handleSave}>{translate('LABEL.SAVE')}</button>
|
||||
</div>
|
||||
<TabSet tabs={tabs} active={activeTarget} onTabChange={handleTabChange}></TabSet>
|
||||
@@ -241,105 +272,37 @@ export default function UserView(props: UserViewProps) {
|
||||
<div className={'user__target-user-table-container'}>
|
||||
<div className={'user__target-user-table'}>
|
||||
<div className={'user__target-user-row user__target-user-table-header'}>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.USERNAME')}</label></div>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.PASSWORD')}</label></div>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.TOKEN')}</label></div>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.SERVER')}</label></div>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.PROXY')}</label></div>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.MAX_CON')}</label></div>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.STATUS')}</label></div>
|
||||
<div className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate('LABEL.EXP_DATE')}</label></div>
|
||||
{COLUMNS.map(col => <div key={col.field}
|
||||
className={'user__target-user-col user__target-user-col-header'}>
|
||||
<label>{translate(col.label)}</label></div>)}
|
||||
<div className={'user__target-user-col user__target-user-col-header'}></div>
|
||||
</div>
|
||||
|
||||
{target.credentials.map((usr, idx) =>
|
||||
{(filteredUser[target.target]?.user ?? target.credentials).map((usr, idx) =>
|
||||
<div key={'credential' + idx} className={'user__target-user-row'}>
|
||||
{['username', 'password', 'token'].map((field) =>
|
||||
<div key={'target_' + target.target + '_' + field + '_' + usr.username}
|
||||
className={'user__target-user-col'}>
|
||||
<div className={'user__target-user-col-label'}>
|
||||
<label>{field.charAt(0).toUpperCase() + field.slice(1)}</label>
|
||||
</div>
|
||||
<input data-target={target.target} data-idx={idx}
|
||||
defaultValue={(usr as any)[field]}
|
||||
data-field={field} onChange={handleValueChange}></input>
|
||||
</div>
|
||||
)}
|
||||
<div key={'target_' + target.target + '_server_' + usr.username}
|
||||
className={'user__target-user-col '}>
|
||||
{COLUMNS.map(c => <div
|
||||
key={'target_' + target.target + '_' + c.field + '_' + usr.username}
|
||||
className={'user__target-user-col'}>
|
||||
<div className={'user__target-user-col-label'}>
|
||||
<label>{translate('LABEL.SERVER')}</label></div>
|
||||
<TagSelect options={serverOptions}
|
||||
name={target.target + '@' + idx + '@server'}
|
||||
defaultValues={(usr as any)?.['server']} radio={true}
|
||||
multi={false} onSelect={handleChange}></TagSelect>
|
||||
</div>
|
||||
<div key={'target_' + target.target + '_proxy_' + usr.username}
|
||||
className={'user__target-user-col '}>
|
||||
<div className={'user__target-user-col-label'}>
|
||||
<label>{translate('LABEL.PROXY')}</label></div>
|
||||
<TagSelect options={proxy_options}
|
||||
name={target.target + '@' + idx + '@proxy'}
|
||||
defaultValues={(usr as any)?.['proxy']} radio={true}
|
||||
multi={false} onSelect={handleChange}></TagSelect>
|
||||
</div>
|
||||
{['max_connections'].map((field) =>
|
||||
<div key={'target_' + target.target + '_' + field + '_' + usr.username}
|
||||
className={'user__target-user-col'}>
|
||||
<div className={'user__target-user-col-label'}>
|
||||
<label>{field.charAt(0).toUpperCase() + field.slice(1)}</label>
|
||||
</div>
|
||||
<input data-target={target.target} data-idx={idx}
|
||||
defaultValue={(usr as any)[field]}
|
||||
className={'user__target-user-col-number'}
|
||||
data-field={field} onChange={handleValueChange}></input>
|
||||
<label>{translate(c.label)}</label>
|
||||
</div>
|
||||
)}
|
||||
<div key={'target_' + target.target + '_status_' + usr.username}
|
||||
className={'user__target-user-col '}>
|
||||
<div className={'user__target-user-col-label'}>
|
||||
<label>{translate('LABEL.STATUS')}</label></div>
|
||||
<select name={target.target + '@' + idx + '@status'}
|
||||
defaultValue={(usr as any)?.['status']}
|
||||
onChange={handleSelectChange}>
|
||||
{status_options.map(option =>
|
||||
<option key={option.value + idx}>{option.label}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
{['exp_date'].map((field) =>
|
||||
<div key={'target_' + target.target + '_' + field + '_' + usr.username}
|
||||
className={'user__target-user-col'}>
|
||||
<div className={'user__target-user-col-label'}>
|
||||
<label>{field.charAt(0).toUpperCase() + field.slice(1)}</label>
|
||||
</div>
|
||||
<DatePicker
|
||||
value={(usr as any)[field]}
|
||||
className={'user__target-user-col-date'}
|
||||
onChange={(date: any) => handleValueChange({
|
||||
target: {
|
||||
value: date,
|
||||
dataset: {
|
||||
target: target.target,
|
||||
idx,
|
||||
field,
|
||||
}
|
||||
},
|
||||
})}></DatePicker>
|
||||
<div className={'user__target-user-col-value'}>{c.hidden &&
|
||||
<span className={'visibility'} data-hiddenkey={usr.username + c.field}
|
||||
onClick={handleVisibility}>
|
||||
{getIconByName('Visibility')}</span>}
|
||||
{c.render((usr as any)[c.field], (showHiddenFields[usr.username + c.field] ?? c.hidden))}
|
||||
</div>
|
||||
)}
|
||||
<div className={'user__target-user-col user__target-user-col-toolbar'}>
|
||||
<span data-target={target.target} data-idx={idx} onClick={handleUserRemove}>
|
||||
</div>)
|
||||
}
|
||||
<div className={'user__target-user-col-toolbar'}>
|
||||
<span data-target={target.target} data-user={usr.username}
|
||||
onClick={handleUserRemove}>
|
||||
{getIconByName('PersonRemove')}
|
||||
</span>
|
||||
<span data-target={target.target} data-user={usr.username}
|
||||
onClick={handleUserEdit}>
|
||||
{getIconByName('Edit')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -49,6 +49,8 @@ const DEFAULT_ICONS = {
|
||||
Logout: 'm17 7-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4z',
|
||||
SelectAll: 'M 2,9.2105263 H 13.578947 V 11.315789 H 2 Z M 2,5 H 13.578947 V 7.105263 H 2 Z m 0,8.421054 h 7.368421 v 2.105263 H 2 Z m 18.515789,-2.178949 -4.473684,4.46316 -2.231579,-2.23158 -1.48421,1.484211 3.715789,3.726316 L 22,12.726317 Z',
|
||||
DeselectAll: 'M 2,5 V 7.1538458 H 13.612371 V 5 Z M 2,9.3076922 V 11.461539 H 13.612371 V 9.3076922 Z m 14.082473,1.7899618 -1.265978,1.392431 2.327832,2.559791 -2.327832,2.557693 L 16.082473,19 18.408247,16.442307 20.734022,19 22,17.607569 19.674225,15.049876 22,12.490085 20.734022,11.097654 18.408247,13.655347 Z M 2,13.615385 v 2.153847 h 7.3896896 v -2.153847 z',
|
||||
Visibility: 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3',
|
||||
Edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ $media-size-mobile: 600px;
|
||||
border-radius: var(--icon-button-border-radius);
|
||||
aspect-ratio: 1 / 1;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
width: min-content;
|
||||
|
||||
svg {
|
||||
color: var(--text-color);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
function formatDate(date: any): string {
|
||||
if (typeof date === 'string') {
|
||||
return date;
|
||||
}
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
const DateUtils = {
|
||||
formatDate: (date: any) => {
|
||||
if (date) {
|
||||
console.log(date);
|
||||
return formatDate(date);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default DateUtils;
|
||||
Reference in New Issue
Block a user