diff --git a/.github/ISSUE_TEMPLATE/feature_story.md b/.github/ISSUE_TEMPLATE/feature_story.md
new file mode 100644
index 0000000..8d94d66
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_story.md
@@ -0,0 +1,21 @@
+---
+name: Feature
+about: Feature plan
+title: Feature title
+labels: enhancement improvement
+assignees: 'Krr0ptioN'
+
+---
+
+## Description
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+### User Interface Specification
+- User interface and Design Specification of the feature
+
+### Technical Interface Specification
+- Domain business logic and technical specification
+
+### Tasks
+
+- [ ] Tasks
diff --git a/.github/workflows/pylint.yml b/.github/workflows/api-ci.yml
similarity index 50%
rename from .github/workflows/pylint.yml
rename to .github/workflows/api-ci.yml
index e44f0a7..b4a006d 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/api-ci.yml
@@ -7,18 +7,31 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10"]
+ python-version: ["3.10"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
+
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
pip install -r requirements.txt
- - name: Analysing the code with pylint
+
+ - name: Lint with pylint
run: |
- pylint $(git ls-files '*.py')
+ pip install pylint
+ OUTPUT=$(pylint ./src --exit-zero)
+ echo "MESSAGE=$OUTPUT" >> $GITHUB_ENV
+
+ - name: Post Lint result to PR
+ uses: mshick/add-pr-comment@v1
+ with:
+ message: ${{ env.MESSAGE }}
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ repo-token-user-login: 'github-actions[bot]'
+ allow-repeats: false
+
diff --git a/.github/workflows/node.js.yml b/.github/workflows/dashboard-ci.yml
similarity index 69%
rename from .github/workflows/node.js.yml
rename to .github/workflows/dashboard-ci.yml
index dbba541..5292952 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/dashboard-ci.yml
@@ -1,15 +1,14 @@
-name: Node.js CI
+name: Dashboard CI
on:
push:
paths:
- 'app/dashboard/**'
- # branches: [ "master" ]
+ branches: [ "master" ]
pull_request:
paths:
- 'app/dashboard/**'
- # branches: [ "master" ]
jobs:
build:
@@ -20,17 +19,23 @@ jobs:
run:
working-directory: ./app/dashboard
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '21.0'
cache: 'npm'
cache-dependency-path: 'app/dashboard/package-lock.json'
- - run: npm ci
- - run: npm run build --if-present -- --outDir build --base '/dashboard/'
- - run: npm test
-
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint Dashboard
+ run: npm ci
+
+ - name: Build Dashboard
+ run: npm run build --if-present -- --outDir build --base '/dashboard/'
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4.2.0
diff --git a/.github/workflows/build.yml b/.github/workflows/package.yml
similarity index 65%
rename from .github/workflows/build.yml
rename to .github/workflows/package.yml
index ea4f204..c70a110 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/package.yml
@@ -1,4 +1,4 @@
-name: Build
+name: Package
on:
push:
@@ -18,12 +18,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- - name: Login to Docker Hub
- uses: docker/login-action@v2
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
@@ -38,7 +32,5 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: |
- gozargah/marzban:latest
- gozargah/marzban:${{github.ref_name}}
- ghcr.io/gozargah/marzban:latest
- ghcr.io/gozargah/marzban:${{github.ref_name}}
+ ghcr.io/khodedawsh/marzneshin:latest
+ ghcr.io/khodedawsh/marzneshin:${{github.ref_name}}
diff --git a/app/dashboard/src/components/UserDialog/DataLimitField.tsx b/app/dashboard/src/components/UserDialog/DataLimitField.tsx
new file mode 100644
index 0000000..3c39a8b
--- /dev/null
+++ b/app/dashboard/src/components/UserDialog/DataLimitField.tsx
@@ -0,0 +1,40 @@
+import { FormControl, FormLabel } from '@chakra-ui/react';
+import { Input } from '../Input';
+import { TFunction } from 'i18next';
+import { Controller } from 'react-hook-form';
+
+
+interface DataLimitFieldProps {
+ form: any;
+ disabled: boolean;
+ t: TFunction<"translation", undefined, "translation">;
+}
+
+
+export const DataLimitField = ({ form, disabled, t }: DataLimitFieldProps) => {
+ return (
+
+ {t("userDialog.dataLimit")}
+ {
+ return (
+
+ );
+ }}
+ />
+
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/ExpireDateField.tsx b/app/dashboard/src/components/UserDialog/ExpireDateField.tsx
new file mode 100644
index 0000000..69bebf7
--- /dev/null
+++ b/app/dashboard/src/components/UserDialog/ExpireDateField.tsx
@@ -0,0 +1,91 @@
+import {
+ FormControl,
+ FormHelperText,
+ FormLabel,
+} from "@chakra-ui/react";
+import dayjs from "dayjs";
+import { i18n, TFunction } from "i18next";
+import ReactDatePicker from "react-datepicker";
+import { Controller } from "react-hook-form";
+import { relativeExpiryDate } from "utils/dateFormatter";
+import { Input } from "../Input";
+
+interface ExpireDateFieldProps {
+ form: any;
+ disabled: boolean;
+ t: TFunction<"translation", undefined, "translation">;
+ i18n: i18n;
+}
+
+export const ExpireDateField = ({ form, t, i18n, disabled }: ExpireDateFieldProps) => {
+ return (
+
+ {t("userDialog.expiryDate")}
+ {
+ function createDateAsUTC(num: number) {
+ return dayjs(
+ dayjs(num * 1000).utc()
+ // .format("MMMM D, YYYY") // exception with: dayjs.locale(lng);
+ ).toDate();
+ }
+ const { status, time } = relativeExpiryDate(
+ field.value
+ );
+ return (
+ <>
+ {
+ field.onChange({
+ target: {
+ value: date
+ ? dayjs(
+ dayjs(date)
+ .set("hour", 23)
+ .set("minute", 59)
+ .set("second", 59)
+ )
+ .utc()
+ .valueOf() / 1000
+ : 0,
+ name: "expire",
+ },
+ });
+ }}
+ customInput={
+
+ }
+ />
+ {field.value ? (
+
+ {t(status, { time: time })}
+
+ ) : (
+ ""
+ )}
+ >
+ );
+ }}
+ />
+
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/NoteField.tsx b/app/dashboard/src/components/UserDialog/NoteField.tsx
new file mode 100644
index 0000000..39a7544
--- /dev/null
+++ b/app/dashboard/src/components/UserDialog/NoteField.tsx
@@ -0,0 +1,22 @@
+import { FormControl, FormLabel, Textarea, FormErrorMessage } from '@chakra-ui/react';
+import { TFunction } from 'i18next';
+
+interface NoteFieldProps {
+ form: any;
+ t: TFunction<"translation", undefined, "translation">;
+}
+
+export const NoteField = ({ t, form }: NoteFieldProps) => {
+ return (
+
+ {t("userDialog.note")}
+
+
+ {form.formState.errors?.note?.message}
+
+
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/PeriodicUsageReset.tsx b/app/dashboard/src/components/UserDialog/PeriodicUsageReset.tsx
new file mode 100644
index 0000000..d476b47
--- /dev/null
+++ b/app/dashboard/src/components/UserDialog/PeriodicUsageReset.tsx
@@ -0,0 +1,40 @@
+import { FormControl, FormLabel, Select } from '@chakra-ui/react';
+import { Controller } from "react-hook-form";
+import { resetStrategy } from "constants/UserSettings";
+import { TFunction } from 'i18next';
+
+
+interface PreiodicUsageResetProps {
+ form: any;
+ t: TFunction<"translation", undefined, "translation">;
+}
+
+export const PeriodicUsageReset = ({ form, t }: PreiodicUsageResetProps) => {
+ return (
+
+
+
+ {t("userDialog.periodicUsageReset")}
+
+ {
+ return (
+
+ );
+ }}
+ />
+
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/ServicePropertyStatus.tsx b/app/dashboard/src/components/UserDialog/ServicePropertyStatus.tsx
new file mode 100644
index 0000000..c753409
--- /dev/null
+++ b/app/dashboard/src/components/UserDialog/ServicePropertyStatus.tsx
@@ -0,0 +1,19 @@
+import { Flex } from "@chakra-ui/react";
+import { FC, useState } from "react";
+import { ChakraIcon } from 'types/ChakraIcon';
+
+
+type ServicePropertyStatusProps = {
+ value: number;
+ StatusIcon: ChakraIcon;
+}
+
+export const ServicePropertyStatus: FC = ({ value, StatusIcon }) => {
+ const [valueState, _] = useState(value);
+ return (
+
+
+ {valueState}
+
+ );
+};
diff --git a/app/dashboard/src/components/UserDialog/ServicesField.tsx b/app/dashboard/src/components/UserDialog/ServicesField.tsx
new file mode 100644
index 0000000..2159259
--- /dev/null
+++ b/app/dashboard/src/components/UserDialog/ServicesField.tsx
@@ -0,0 +1,45 @@
+import { FormControl, FormLabel, FormErrorMessage, Center, Spinner } from '@chakra-ui/react';
+import { TFunction } from 'i18next';
+import { Controller } from 'react-hook-form';
+import { Service } from 'types/Service';
+import { ServicesSelectList } from './ServicesSelectList';
+
+interface ServicesFieldProps {
+ form: any;
+ services: Service[];
+ disabled: boolean;
+ t: TFunction<"translation", undefined, "translation">;
+}
+
+export const ServicesField = ({ form, disabled, t, services }: ServicesFieldProps) => {
+ return services.length === 0 ? (
+
+ ) : (
+
+ {t("userDialog.services")}
+ {
+ return (
+
+ );
+ }}
+ />
+
+ {t(
+ form.formState.errors.services
+ ?.message as string
+ )}
+
+
+ );
+}
diff --git a/app/dashboard/src/components/ServicesSelectList.tsx b/app/dashboard/src/components/UserDialog/ServicesSelectList.tsx
similarity index 89%
rename from app/dashboard/src/components/ServicesSelectList.tsx
rename to app/dashboard/src/components/UserDialog/ServicesSelectList.tsx
index bf57c49..e287d8e 100644
--- a/app/dashboard/src/components/ServicesSelectList.tsx
+++ b/app/dashboard/src/components/UserDialog/ServicesSelectList.tsx
@@ -12,8 +12,8 @@ import { FC, forwardRef, Ref, useState } from "react";
import {
useFormContext,
} from "react-hook-form";
-import { ChakraIcon } from "types/ChakraIcon";
import { Service } from "types/Service";
+import { ServicePropertyStatus } from './ServicePropertyStatus';
const ServiceIconBaseStyle = {
baseStyle: {
@@ -29,21 +29,6 @@ const UsersServiceIcon = chakra(UsersIcon, ServiceIconBaseStyle);
const InboundsServiceIcon = chakra(ServerStackIcon, ServiceIconBaseStyle);
-type ServicePropertyStatusProps = {
- value: number;
- StatusIcon: ChakraIcon;
-}
-
-const ServicePropertyStatus: FC = ({ value, StatusIcon }) => {
- const [valueState, _] = useState(value);
- return (
-
-
- {valueState}
-
- );
-};
-
type CustomCheckboxProps = {
isChecked: boolean;
onChange: () => void;
diff --git a/app/dashboard/src/components/UserDialog/UsernameField.tsx b/app/dashboard/src/components/UserDialog/UsernameField.tsx
new file mode 100644
index 0000000..60df887
--- /dev/null
+++ b/app/dashboard/src/components/UserDialog/UsernameField.tsx
@@ -0,0 +1,63 @@
+import { HStack, Input, Tooltip, Box, Switch, FormControl, FormLabel } from '@chakra-ui/react';
+import { TFunction } from 'i18next';
+import { Controller } from "react-hook-form";
+
+interface UsernameFieldProps {
+ form: any;
+ isEditing: boolean;
+ t: TFunction<"translation", undefined, "translation">;
+ disabled: boolean;
+}
+
+export const UsernameField = ({ form, isEditing, t, disabled }: UsernameFieldProps) => {
+ return (
+
+ {t("username")}
+
+
+ {isEditing && (
+
+ {
+ return (
+
+
+ {
+ if (e.target.checked) {
+ field.onChange("active");
+ } else {
+ field.onChange("disabled");
+ }
+ }}
+ />
+
+
+ );
+ }}
+ />
+
+ )}
+
+
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/index.tsx b/app/dashboard/src/components/UserDialog/index.tsx
index 27a93f1..4193477 100644
--- a/app/dashboard/src/components/UserDialog/index.tsx
+++ b/app/dashboard/src/components/UserDialog/index.tsx
@@ -4,62 +4,55 @@ import {
Box,
Collapse,
Flex,
- FormControl,
- FormErrorMessage,
- FormHelperText,
- FormLabel,
Grid,
GridItem,
- HStack,
ModalBody,
ModalCloseButton,
ModalContent,
Modal,
- Select,
- Switch,
- Textarea,
- Tooltip,
VStack,
useColorMode,
useToast,
- Spinner,
ModalOverlay,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
-import { resetStrategy } from "constants/UserSettings";
import { FilterUsageType, useDashboard } from "contexts/DashboardContext";
import dayjs from "dayjs";
import { FC, useEffect, useState } from "react";
import ReactApexChart from "react-apexcharts";
-import ReactDatePicker from "react-datepicker";
-import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
+import { FormProvider, useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
User,
UserCreate,
} from "types/User";
-import { relativeExpiryDate } from "utils/dateFormatter";
-import { Input } from "../Input";
-import { ServicesSelectList } from "../ServicesSelectList";
import { UsageFilter, createUsageConfig } from "../UsageFilter";
import { AddUserIcon, EditUserIcon } from "./UserDialogIcons";
-import { DevTool } from "@hookform/devtools";
import { UserDialogModalHeader } from "./ModalHeader";
import { UserDialogModalFooter } from "./ModalFooter";
import { getDefaultValues } from "./DefaultValues";
import { schema, FormType } from "./FormSchema";
-
+import { ServicesField } from "./ServicesField";
+import { UsernameField } from "./UsernameField";
+import { DataLimitField } from "./DataLimitField";
+import { PeriodicUsageReset as PeriodicUsageReset } from "./PeriodicUsageReset";
+import { ExpireDateField } from "./ExpireDateField";
+import { NoteField } from "./NoteField";
+import { Service } from "types/Service";
const formatUser = (user: User): FormType => {
+ const services: number[] = user.services.map((service: number | Service): number => {
+ return (typeof service !== 'number') ? service.id : service;
+ });
return {
...user,
+ services,
data_limit: user.data_limit
? Number((user.data_limit / 1073741824).toFixed(5))
: user.data_limit,
};
};
-
export type UserDialogProps = {};
export const UserDialog: FC = () => {
@@ -136,9 +129,6 @@ export const UserDialog: FC = () => {
const methods = { edited: editUser, created: createUser };
const method = isEditing ? "edited" : "created";
setError(null);
- console.log('Form errors:', form.formState.errors);
-
- console.log("For Value", values);
const { services, username, ...rest } = values;
let body: UserCreate = {
@@ -233,186 +223,19 @@ export const UserDialog: FC = () => {
gridAutoRows="min-content"
w="full"
>
-
- {t("username")}
-
-
- {isEditing && (
-
- {
- return (
-
-
- {
- if (e.target.checked) {
- field.onChange("active");
- } else {
- field.onChange("disabled");
- }
- }}
- />
-
-
- );
- }}
- />
-
- )}
-
-
-
- {t("userDialog.dataLimit")}
- {
- return (
-
- );
- }}
- />
-
+
+
+
0)}
animateOpacity
style={{ width: "100%" }}
>
-
-
- {t("userDialog.periodicUsageReset")}
-
- {
- return (
-
- );
- }}
- />
-
+
-
- {t("userDialog.expiryDate")}
- {
- function createDateAsUTC(num: number) {
- return dayjs(
- dayjs(num * 1000).utc()
- // .format("MMMM D, YYYY") // exception with: dayjs.locale(lng);
- ).toDate();
- }
- const { status, time } = relativeExpiryDate(
- field.value
- );
- return (
- <>
- {
- field.onChange({
- target: {
- value: date
- ? dayjs(
- dayjs(date)
- .set("hour", 23)
- .set("minute", 59)
- .set("second", 59)
- )
- .utc()
- .valueOf() / 1000
- : 0,
- name: "expire",
- },
- });
- }}
- customInput={
-
- }
- />
- {field.value ? (
-
- {t(status, { time: time })}
-
- ) : (
- ""
- )}
- >
- );
- }}
- />
-
+
+
-
- {t("userDialog.note")}
-
-
- {form.formState.errors?.note?.message}
-
-
{error && (
= () => {
- {services.length === 0 ? (
-
- ) :
-
- {t("userDialog.services")}
- {
- return (
-
- );
- }}
- />
-
- {t(
- form.formState.errors.services
- ?.message as string
- )}
-
-
- }
+
{isEditing && usageVisible && (
diff --git a/app/dashboard/src/contexts/DashboardContext.tsx b/app/dashboard/src/contexts/DashboardContext.tsx
index 73adb06..87091d5 100644
--- a/app/dashboard/src/contexts/DashboardContext.tsx
+++ b/app/dashboard/src/contexts/DashboardContext.tsx
@@ -81,6 +81,9 @@ export const fetchUsers = async (query: FilterType): Promise => {
useDashboard.setState({ loading: true });
return fetch("/users", { query })
.then((users) => {
+ for (let i = 0; i < users.users.length; i++) {
+ users.users[i].service = users.users[i].service_ids;
+ }
useDashboard.setState({ users });
return users;
})