From 715d275a7e000cc0ef37e102475b160ab2db834d Mon Sep 17 00:00:00 2001 From: euzu Date: Tue, 6 May 2025 12:05:46 +0200 Subject: [PATCH 1/3] ui user view refactor, added status filter --- frontend/public/assets/i18n/en_common.json | 5 +- frontend/src/app/app.scss | 1 + .../playlist-browser/playlist-browser.scss | 2 +- .../component/preferences/preferences.scss | 2 + .../src/component/status-view/status-view.tsx | 11 +- .../src/component/user-editor/user-editor.tsx | 2 +- .../src/component/user-view/user-view.scss | 19 ++- .../src/component/user-view/user-view.tsx | 124 +++++++++++------- frontend/src/scss/_common.scss | 2 +- frontend/src/scss/_default.scss | 3 + frontend/src/scss/_theme.scss | 1 + src/api/model/provider_config.rs | 7 +- src/model/config_ip_check.rs | 7 +- src/utils/network/ip_checker.rs | 1 - 14 files changed, 124 insertions(+), 63 deletions(-) diff --git a/frontend/public/assets/i18n/en_common.json b/frontend/public/assets/i18n/en_common.json index 94e4fccc8..f85ec3291 100644 --- a/frontend/public/assets/i18n/en_common.json +++ b/frontend/public/assets/i18n/en_common.json @@ -111,7 +111,9 @@ "PATTERN_IPV6": "Pattern IPv6", "IPv4": "IPv4", "IPv6": "IPv6", - "IP": "IP" + "IP": "IP", + "TARGET": "Target", + "TARGETS": "Targets" }, "TITLE": { "USER_BOUQUET_EDITOR": "User group editor" @@ -120,6 +122,7 @@ "NO_CONTENT": "No content", "INVALID_REGEXP": "Invalid regexp", "INVALID_FILETYPE": "Invalid filetype", + "TARGET_NOT_EXISTS": "Target does not exist", "DOWNLOAD": { "SUCCESS": "Successfully downloaded", "FAIL": "Failed to download!", diff --git a/frontend/src/app/app.scss b/frontend/src/app/app.scss index 9035d7e89..d9f125997 100755 --- a/frontend/src/app/app.scss +++ b/frontend/src/app/app.scss @@ -19,6 +19,7 @@ color: var(--app-header-color); align-content: center; padding: 10px; + max-width: var(--max-view-width) !important; @media (max-width: common.$media-size-mobile) { padding: 10px 0; } diff --git a/frontend/src/component/playlist-browser/playlist-browser.scss b/frontend/src/component/playlist-browser/playlist-browser.scss index 4e93c2b78..b68eb5088 100644 --- a/frontend/src/component/playlist-browser/playlist-browser.scss +++ b/frontend/src/component/playlist-browser/playlist-browser.scss @@ -20,7 +20,7 @@ flex: 0 0 auto; box-sizing: border-box; overflow: hidden; - max-width: 100%; + max-width: var(--max-view-width); background-color: inherit; .source-selector { diff --git a/frontend/src/component/preferences/preferences.scss b/frontend/src/component/preferences/preferences.scss index bbebf79b3..08d5b0f9e 100644 --- a/frontend/src/component/preferences/preferences.scss +++ b/frontend/src/component/preferences/preferences.scss @@ -4,11 +4,13 @@ @include common.container_horz_no_padding(); overflow: hidden; flex: 1 1 auto; + max-width: var(--max-view-width) !important; &__content { @include common.container_horz_nowrap(flex-start, 0); flex: 1 1 auto; overflow: hidden; + gap: 0; } &__panels { diff --git a/frontend/src/component/status-view/status-view.tsx b/frontend/src/component/status-view/status-view.tsx index fe17b6f09..08cb9701a 100644 --- a/frontend/src/component/status-view/status-view.tsx +++ b/frontend/src/component/status-view/status-view.tsx @@ -34,10 +34,17 @@ export default function StatusView() { }, [services]) useEffect(() => { - const sub = interval(REQUEST_INTERVAL).subscribe(() => services.status().getServerStatus().pipe(first()).subscribe((data: any) => setStatus(data))); + const fetchData = () => services.status().getServerStatus().pipe(first()).subscribe({ + next: (data: any) => setStatus(data), + error: () => { + } + }); + fetchData(); + const sub = interval(REQUEST_INTERVAL).subscribe(fetchData); checkIp(); return () => sub.unsubscribe(); - }, [services, checkIp]); + }, [services, checkIp]) + return
{ipCheck && <> diff --git a/frontend/src/component/user-editor/user-editor.tsx b/frontend/src/component/user-editor/user-editor.tsx index c287976b6..3d5b523ed 100644 --- a/frontend/src/component/user-editor/user-editor.tsx +++ b/frontend/src/component/user-editor/user-editor.tsx @@ -4,7 +4,7 @@ import {Credentials} from "../../model/server-config"; import useTranslator from "../../hook/use-translator"; import FormView, {FormFieldType} from "../form-view/from-view"; -const STATUS_OPTIONS = [ +export const STATUS_OPTIONS = [ {value: 'Active', label: 'Active'}, {value: 'Expired', label: 'Expired'}, {value: 'Banned', label: 'Banned'}, diff --git a/frontend/src/component/user-view/user-view.scss b/frontend/src/component/user-view/user-view.scss index 9783fc436..905124c0e 100644 --- a/frontend/src/component/user-view/user-view.scss +++ b/frontend/src/component/user-view/user-view.scss @@ -1,6 +1,10 @@ @use '../../scss/common'; @use '../../scss/preferences'; +.label { + color: var(--label-color); +} + .proxy-type { &-reverse { } @@ -49,12 +53,8 @@ } label { font-weight: bold; - - &:not(.target-not-exists) { - color: var(--label-color); - } - - &.target-not-exists { + color: var(--label-color); + span.target-not-exists { color: var(--attention-color); } } @@ -88,6 +88,10 @@ display: flex; flex-flow: row; padding: 8px; + gap: 12px; + span { + color: var(--label-color); + } } label { @@ -143,6 +147,9 @@ &-col-header { padding: 8px; background-color: var(--background-color); + &-tools { + width: 64px !important; + } } &-col { diff --git a/frontend/src/component/user-view/user-view.tsx b/frontend/src/component/user-view/user-view.tsx index 748513f30..8e352b208 100644 --- a/frontend/src/component/user-view/user-view.tsx +++ b/frontend/src/component/user-view/user-view.tsx @@ -1,4 +1,4 @@ -import React, {JSX, ReactNode, useCallback, useEffect, useRef, useState} from "react"; +import React, {JSX, useCallback, useEffect, useRef, useState} from "react"; import './user-view.scss'; import ServerConfig, {Credentials, TargetUser} from "../../model/server-config"; import {getIconByName} from "../../icons/icons"; @@ -9,7 +9,7 @@ import TabSet, {TabSetTab} from "../tab-set/tab-set"; import useTranslator from "../../hook/use-translator"; import DateUtils from "../../utils/date-utils"; import PlaylistFilter from "../playlist-filter/playlist-filter"; -import UserEditor from "../user-editor/user-editor"; +import UserEditor, {STATUS_OPTIONS} from "../user-editor/user-editor"; import TextGenerator from "../../utils/text-generator"; const renderExpDate = (value: any, hidden?: boolean) => { @@ -20,7 +20,8 @@ const renderExpDate = (value: any, hidden?: boolean) => { }; const renderBool = (value: any, hidden?: boolean) => { - return {getIconByName(value ? 'CheckMark' : 'Clear' )}; + return {getIconByName(value ? 'CheckMark' : 'Clear')}; }; const renderMaxCon = (value: any, hidden?: boolean) => { @@ -79,8 +80,8 @@ const prepareTargetUserForSave = (targetUser: TargetUser[]): TargetUser[] => { return targetUser.map((user) => { let storeUser = {...user, credentials: user.credentials.map(c => ({...c}))}; storeUser.credentials.forEach((credential) => { - credential.exp_date = DateUtils.toUnixSeconds(credential.exp_date); - credential.created_at = DateUtils.toUnixSeconds(credential.created_at); + credential.exp_date = DateUtils.toUnixSeconds(credential.exp_date); + credential.created_at = DateUtils.toUnixSeconds(credential.created_at); }); return storeUser; }); @@ -130,19 +131,19 @@ const createNewUser = (targets: TargetUser[]): Credentials => { } const created_at = Date.now(); const exp_date = new Date(); - exp_date.setFullYear(exp_date.getFullYear()+1); + exp_date.setFullYear(exp_date.getFullYear() + 1); return { - username, - password: TextGenerator.generatePassword(), - token: TextGenerator.generatePassword(), - proxy: 'reverse', - created_at, - exp_date: exp_date.getTime(), - max_connections: 1, - status: "Active", - ui_enabled: true, - // @ts-ignore - _ref: undefined, // an indicator for new user + username, + password: TextGenerator.generatePassword(), + token: TextGenerator.generatePassword(), + proxy: 'reverse', + created_at, + exp_date: exp_date.getTime(), + max_connections: 1, + status: "Active", + ui_enabled: true, + // @ts-ignore + _ref: undefined, // an indicator for new user }; } @@ -165,6 +166,7 @@ export default function UserView(props: UserViewProps) { const [filteredUser, setFilteredUser] = useState>({}); @@ -291,32 +293,44 @@ export default function UserView(props: UserViewProps) { setShowHiddenFields((hiddenFields: any) => ({...hiddenFields, [key]: !hiddenFields[key]})); }, []); - const handleFilter = useCallback((filter: string, regexp: boolean): void => { - let filter_value = regexp ? filter : filter.toLowerCase(); - const target = targets.find(t => t.target === activeTarget); - + const filterTarget = useCallback((target: TargetUser, filter: string, regexp: boolean, filterForStatus: string): void => { if (target) { - if (filter_value?.length) { - const filtered = target.credentials.filter((credential: Credentials) => { + if (filter?.length) { + let filtered = target.credentials.filter((credential: Credentials) => { if (regexp) { // eslint-disable-next-line eqeqeq - return credential.username.trim().match(filter_value) != undefined; + return credential.username.trim().match(filter) != undefined; } else { - return (credential.username.trim().toLowerCase().indexOf(filter_value) > -1); + return (credential.username.trim().toLowerCase().indexOf(filter) > -1); } }) ?? []; + if (filterForStatus) { + filtered = filtered.filter((c: Credentials) => c.status === filterForStatus); + } setFilteredUser((filteredUser: any) => ({ ...filteredUser, - [activeTarget]: {filter, regexp, user: filtered} + [activeTarget]: {filter, regexp, status: filterForStatus, user: filtered} })); } else { + let filtered = undefined; + if (filterForStatus) { + filtered = {filter: undefined, regexp: false, status: filterForStatus, user: target.credentials.filter((c: Credentials) => c.status === filterForStatus)}; + } setFilteredUser((filteredUser: any) => ({ - ...filteredUser, [activeTarget]: undefined + ...filteredUser, [activeTarget]: filtered })); } - } - }, [activeTarget, targets]); + }, [activeTarget]); + + 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) { + let currentFilter = filteredUser[target.target]; + filterTarget(target, filter_value, regexp, currentFilter?.status); + } + }, [activeTarget, targets, filterTarget, filteredUser]); const handleUserEditorSubmit = useCallback((user: Credentials, target_name: string): boolean => { const userRef = (user as any)._ref; @@ -349,14 +363,25 @@ export default function UserView(props: UserViewProps) { const renderTargetOptions = useCallback((target: string): JSX.Element => { let options = targetOptions?.[target]; if (options?.force_redirect?.length > 2) { - return
{translate('HINT.CONFIG.USER.TARGET_FORCE_REDIRECT')} {options.force_redirect}
; + return
{translate('HINT.CONFIG.USER.TARGET_FORCE_REDIRECT')} {options.force_redirect}
; } return <>; }, [targetOptions, translate]); - return
+ const handleStatusFilter = useCallback((evt: any) => { + const target_name = evt.target.dataset.target; + let target = targets.find((t) => t.target === target_name); + if (target) { + let value = evt.target.value; + value = value?.length ? value : undefined; + let currentFilter = filteredUser[target_name]; + filterTarget(target, currentFilter?.filter, !!currentFilter?.regexp, value) + } + }, [filteredUser, filterTarget, targets]); -
+ return
+
@@ -367,10 +392,18 @@ export default function UserView(props: UserViewProps) { targets?.map(target =>
-