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..97fb79198 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}
+ [target.target]: {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, [target.target]: filtered
}));
}
-
}
- }, [activeTarget, targets]);
+ }, []);
+
+ 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 =>
-