From cb71b4f32f9319df5046b405f8bfae8ce96e6d57 Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 24 Feb 2025 16:21:59 +0100 Subject: [PATCH] refactored user-view --- frontend/src/app-user/app-user.tsx | 2 +- frontend/src/app/app.tsx | 2 +- .../component/input-field/input-field.scss | 30 +- .../playlist-filter/playlist-filter.scss | 7 +- .../playlist-filter/playlist-filter.tsx | 17 +- frontend/src/component/svg-icon/svg-icon.tsx | 2 +- .../component/user-playlist/user-playlist.tsx | 26 +- .../src/component/user-view/user-view.scss | 36 +- .../src/component/user-view/user-view.tsx | 313 ++++++++---------- frontend/src/icons/icons.tsx | 2 + frontend/src/scss/_common.scss | 3 + frontend/src/utils/date-utils.ts | 22 ++ 12 files changed, 233 insertions(+), 229 deletions(-) create mode 100644 frontend/src/utils/date-utils.ts diff --git a/frontend/src/app-user/app-user.tsx b/frontend/src/app-user/app-user.tsx index 74fbd6337..50473f327 100755 --- a/frontend/src/app-user/app-user.tsx +++ b/frontend/src/app-user/app-user.tsx @@ -37,7 +37,7 @@ export default function AppUser(props: AppUserProps) {
{appLogo}{appTitle}
-
+
diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx index fe0098e9e..0cb11f44d 100755 --- a/frontend/src/app/app.tsx +++ b/frontend/src/app/app.tsx @@ -165,7 +165,7 @@ export default function App(props: AppProps) {
{appLogo}{appTitle}
-
+
diff --git a/frontend/src/component/input-field/input-field.scss b/frontend/src/component/input-field/input-field.scss index 1a5661a0d..724bee0ff 100644 --- a/frontend/src/component/input-field/input-field.scss +++ b/frontend/src/component/input-field/input-field.scss @@ -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; } } } diff --git a/frontend/src/component/playlist-filter/playlist-filter.scss b/frontend/src/component/playlist-filter/playlist-filter.scss index f39f761f1..b331d0485 100755 --- a/frontend/src/component/playlist-filter/playlist-filter.scss +++ b/frontend/src/component/playlist-filter/playlist-filter.scss @@ -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; diff --git a/frontend/src/component/playlist-filter/playlist-filter.tsx b/frontend/src/component/playlist-filter/playlist-filter.tsx index 1b694c511..67bc8c573 100755 --- a/frontend/src/component/playlist-filter/playlist-filter.tsx +++ b/frontend/src/component/playlist-filter/playlist-filter.tsx @@ -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(undefined); const [useRegexp, setUseRegexp] = useState(false); const [errorMsg, setErrorMsg] = useState(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
- + diff --git a/frontend/src/component/svg-icon/svg-icon.tsx b/frontend/src/component/svg-icon/svg-icon.tsx index 8a374f580..dd411186b 100644 --- a/frontend/src/component/svg-icon/svg-icon.tsx +++ b/frontend/src/component/svg-icon/svg-icon.tsx @@ -7,7 +7,7 @@ interface SvgIconProps { } function SvgIcon(props: SvgIconProps): React.JSX.Element { - return
- {CATEGORY_TABS.map(tab =>
+ {CATEGORY_TABS.map(tab =>
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