diff --git a/frontend/src/component/user-view/user-view.scss b/frontend/src/component/user-view/user-view.scss
index 715ebe0cf..425b88a84 100644
--- a/frontend/src/component/user-view/user-view.scss
+++ b/frontend/src/component/user-view/user-view.scss
@@ -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;
- }
}
diff --git a/frontend/src/component/user-view/user-view.tsx b/frontend/src/component/user-view/user-view.tsx
index 6b16a4585..b1c6ce28b 100644
--- a/frontend/src/component/user-view/user-view.tsx
+++ b/frontend/src/component/user-view/user-view.tsx
@@ -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
([]);
const [activeTarget, setActiveTarget] = useState(undefined);
const [tabs, setTabs] = useState([]);
- 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>({});
+ const [filteredUser, setFilteredUser] = useState>({});
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
+
@@ -241,105 +272,37 @@ export default function UserView(props: UserViewProps) {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {COLUMNS.map(col =>
+
)}
- {target.credentials.map((usr, idx) =>
+ {(filteredUser[target.target]?.user ?? target.credentials).map((usr, idx) =>
- {['username', 'password', 'token'].map((field) =>
-
-
-
-
-
-
- )}
-
+ {COLUMNS.map(c =>
-
-
-
-
-
-
-
-
- {['max_connections'].map((field) =>
-
-
-
-
-
+
- )}
-
-
-
-
-
- {['exp_date'].map((field) =>
-
-
-
-
-
handleValueChange({
- target: {
- value: date,
- dataset: {
- target: target.target,
- idx,
- field,
- }
- },
- })}>
+
{c.hidden &&
+
+ {getIconByName('Visibility')}}
+ {c.render((usr as any)[c.field], (showHiddenFields[usr.username + c.field] ?? c.hidden))}
- )}
-
-
+
)
+ }
+
+
{getIconByName('PersonRemove')}
+
+ {getIconByName('Edit')}
+
)}
diff --git a/frontend/src/icons/icons.tsx b/frontend/src/icons/icons.tsx
index 94d674e67..86221faf0 100644
--- a/frontend/src/icons/icons.tsx
+++ b/frontend/src/icons/icons.tsx
@@ -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'
}
diff --git a/frontend/src/scss/_common.scss b/frontend/src/scss/_common.scss
index 518900292..ef97ab865 100644
--- a/frontend/src/scss/_common.scss
+++ b/frontend/src/scss/_common.scss
@@ -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);
diff --git a/frontend/src/utils/date-utils.ts b/frontend/src/utils/date-utils.ts
new file mode 100644
index 000000000..8c9600616
--- /dev/null
+++ b/frontend/src/utils/date-utils.ts
@@ -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;
\ No newline at end of file