diff --git a/.github/ISSUE_TEMPLATE/feature_story.md 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
rename from .github/workflows/pylint.yml
rename to .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
rename from .github/workflows/node.js.yml
rename to .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
rename from .github/workflows/build.yml
rename to .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
@@ -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 (
+ <FormControl mb={"10px"}>
+ <FormLabel>{t("userDialog.dataLimit")}</FormLabel>
+ <Controller
+ control={form.control}
+ name="data_limit"
+ render={({ field }) => {
+ return (
+ <Input
+ endAdornment="GB"
+ type="number"
+ size="sm"
+ borderRadius="6px"
+ onChange={field.onChange}
+ disabled={disabled}
+ error={
+ form.formState.errors.data_limit?.message
+ }
+ value={field.value ? String(field.value) : ""}
+ />
+ );
+ }}
+ />
+ </FormControl>
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/ExpireDateField.tsx 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 (
+ <FormControl mb={"10px"}>
+ <FormLabel>{t("userDialog.expiryDate")}</FormLabel>
+ <Controller
+ name="expire"
+ control={form.control}
+ render={({ field }) => {
+ 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 (
+ <>
+ <ReactDatePicker
+ locale={i18n.language.toLocaleLowerCase()}
+ dateFormat={t("dateFormat")}
+ minDate={new Date()}
+ selected={
+ field.value
+ ? createDateAsUTC(field.value)
+ : undefined
+ }
+ onChange={(date: Date) => {
+ field.onChange({
+ target: {
+ value: date
+ ? dayjs(
+ dayjs(date)
+ .set("hour", 23)
+ .set("minute", 59)
+ .set("second", 59)
+ )
+ .utc()
+ .valueOf() / 1000
+ : 0,
+ name: "expire",
+ },
+ });
+ }}
+ customInput={
+ <Input
+ size="sm"
+ type="text"
+ borderRadius="6px"
+ clearable
+ disabled={disabled}
+ error={
+ form.formState.errors.expire?.message
+ }
+ />
+ }
+ />
+ {field.value ? (
+ <FormHelperText>
+ {t(status, { time: time })}
+ </FormHelperText>
+ ) : (
+ ""
+ )}
+ </>
+ );
+ }}
+ />
+ </FormControl>
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/NoteField.tsx 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 (
+ <FormControl
+ mb={"10px"}
+ isInvalid={!!form.formState.errors.note}
+ >
+ <FormLabel>{t("userDialog.note")}</FormLabel>
+ <Textarea {...form.register("note")} />
+ <FormErrorMessage>
+ {form.formState.errors?.note?.message}
+ </FormErrorMessage>
+ </FormControl>
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/PeriodicUsageReset.tsx 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 (
+
+ <FormControl height="66px">
+ <FormLabel>
+ {t("userDialog.periodicUsageReset")}
+ </FormLabel>
+ <Controller
+ control={form.control}
+ name="data_limit_reset_strategy"
+ render={({ field }) => {
+ return (
+ <Select size="sm" {...field}>
+ {resetStrategy.map((s) => {
+ return (
+ <option key={s.value} value={s.value}>
+ {t(
+ "userDialog.resetStrategy" + s.title
+ )}
+ </option>
+ );
+ })}
+ </Select>
+ );
+ }}
+ />
+ </FormControl>
+ )
+}
diff --git a/app/dashboard/src/components/UserDialog/ServicePropertyStatus.tsx 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<ServicePropertyStatusProps> = ({ value, StatusIcon }) => {
+ const [valueState, _] = useState(value);
+ return (
+ <Flex flexDirection="row">
+ <StatusIcon />
+ <span>{valueState}</span>
+ </Flex>
+ );
+};
diff --git a/app/dashboard/src/components/UserDialog/ServicesField.tsx 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 ? (
+ <Center><Spinner /></Center>
+ ) : (
+ <FormControl
+ isInvalid={
+ !!form.formState.errors.services?.message
+ }
+ >
+ <FormLabel>{t("userDialog.services")}</FormLabel>
+ <Controller
+ control={form.control}
+ name="services"
+ render={({ field }) => {
+ return (
+ <ServicesSelectList
+ list={services}
+ disabled={disabled}
+ {...field}
+ />
+ );
+ }}
+ />
+ <FormErrorMessage>
+ {t(
+ form.formState.errors.services
+ ?.message as string
+ )}
+ </FormErrorMessage>
+ </FormControl>
+ );
+}
diff --git a/app/dashboard/src/components/ServicesSelectList.tsx b/app/dashboard/src/components/UserDialog/ServicesSelectList.tsx
rename from app/dashboard/src/components/ServicesSelectList.tsx
rename to 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<ServicePropertyStatusProps> = ({ value, StatusIcon }) => {
- const [valueState, _] = useState(value);
- return (
- <Flex flexDirection="row">
- <StatusIcon />
- <span>{valueState}</span>
- </Flex>
- );
-};
-
type CustomCheckboxProps = {
isChecked: boolean;
onChange: () => void;
diff --git a/app/dashboard/src/components/UserDialog/UsernameField.tsx 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 (
+ <FormControl mb={"10px"}>
+ <FormLabel>{t("username")}</FormLabel>
+ <HStack>
+ <Input
+ size="sm"
+ type="text"
+ borderRadius="6px"
+ error={form.formState.errors.username?.message}
+ disabled={disabled || isEditing}
+ {...form.register("username")}
+ />
+ {isEditing && (
+ <HStack px={1}>
+ <Controller
+ name="status"
+ control={form.control}
+ render={({ field }) => {
+ return (
+ <Tooltip
+ placement="top"
+ label={"status: " + field.value}
+ textTransform="capitalize"
+ >
+ <Box>
+ <Switch
+ colorScheme="primary"
+ disabled={
+ field.value !== "active" &&
+ field.value !== "disabled"
+ }
+ isChecked={field.value === "active"}
+ onChange={(e) => {
+ if (e.target.checked) {
+ field.onChange("active");
+ } else {
+ field.onChange("disabled");
+ }
+ }}
+ />
+ </Box>
+ </Tooltip>
+ );
+ }}
+ />
+ </HStack>
+ )}
+ </HStack>
+ </FormControl>
+ )
+}
diff --git 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<UserDialogProps> = () => {
@@ -136,9 +129,6 @@ export const UserDialog: FC<UserDialogProps> = () => {
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<UserDialogProps> = () => {
gridAutoRows="min-content"
w="full"
>
- <FormControl mb={"10px"}>
- <FormLabel>{t("username")}</FormLabel>
- <HStack>
- <Input
- size="sm"
- type="text"
- borderRadius="6px"
- error={form.formState.errors.username?.message}
- disabled={disabled || isEditing}
- {...form.register("username")}
- />
- {isEditing && (
- <HStack px={1}>
- <Controller
- name="status"
- control={form.control}
- render={({ field }) => {
- return (
- <Tooltip
- placement="top"
- label={"status: " + field.value}
- textTransform="capitalize"
- >
- <Box>
- <Switch
- colorScheme="primary"
- disabled={
- field.value !== "active" &&
- field.value !== "disabled"
- }
- isChecked={field.value === "active"}
- onChange={(e) => {
- if (e.target.checked) {
- field.onChange("active");
- } else {
- field.onChange("disabled");
- }
- }}
- />
- </Box>
- </Tooltip>
- );
- }}
- />
- </HStack>
- )}
- </HStack>
- </FormControl>
- <FormControl mb={"10px"}>
- <FormLabel>{t("userDialog.dataLimit")}</FormLabel>
- <Controller
- control={form.control}
- name="data_limit"
- render={({ field }) => {
- return (
- <Input
- endAdornment="GB"
- type="number"
- size="sm"
- borderRadius="6px"
- onChange={field.onChange}
- disabled={disabled}
- error={
- form.formState.errors.data_limit?.message
- }
- value={field.value ? String(field.value) : ""}
- />
- );
- }}
- />
- </FormControl>
+ <UsernameField form={form} disabled={disabled} isEditing={isEditing} t={t} />
+ <DataLimitField form={form} disabled={disabled} t={t} />
+
<Collapse
in={!!(dataLimit && dataLimit > 0)}
animateOpacity
style={{ width: "100%" }}
>
- <FormControl height="66px">
- <FormLabel>
- {t("userDialog.periodicUsageReset")}
- </FormLabel>
- <Controller
- control={form.control}
- name="data_limit_reset_strategy"
- render={({ field }) => {
- return (
- <Select size="sm" {...field}>
- {resetStrategy.map((s) => {
- return (
- <option key={s.value} value={s.value}>
- {t(
- "userDialog.resetStrategy" + s.title
- )}
- </option>
- );
- })}
- </Select>
- );
- }}
- />
- </FormControl>
+ <PeriodicUsageReset form={form} t={t} />
</Collapse>
- <FormControl mb={"10px"}>
- <FormLabel>{t("userDialog.expiryDate")}</FormLabel>
- <Controller
- name="expire"
- control={form.control}
- render={({ field }) => {
- 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 (
- <>
- <ReactDatePicker
- locale={i18n.language.toLocaleLowerCase()}
- dateFormat={t("dateFormat")}
- minDate={new Date()}
- selected={
- field.value
- ? createDateAsUTC(field.value)
- : undefined
- }
- onChange={(date: Date) => {
- field.onChange({
- target: {
- value: date
- ? dayjs(
- dayjs(date)
- .set("hour", 23)
- .set("minute", 59)
- .set("second", 59)
- )
- .utc()
- .valueOf() / 1000
- : 0,
- name: "expire",
- },
- });
- }}
- customInput={
- <Input
- size="sm"
- type="text"
- borderRadius="6px"
- clearable
- disabled={disabled}
- error={
- form.formState.errors.expire?.message
- }
- />
- }
- />
- {field.value ? (
- <FormHelperText>
- {t(status, { time: time })}
- </FormHelperText>
- ) : (
- ""
- )}
- </>
- );
- }}
- />
- </FormControl>
+ <ExpireDateField form={form} t={t} i18n={i18n} disabled={disabled} />
+ <NoteField form={form} t={t} />
- <FormControl
- mb={"10px"}
- isInvalid={!!form.formState.errors.note}
- >
- <FormLabel>{t("userDialog.note")}</FormLabel>
- <Textarea {...form.register("note")} />
- <FormErrorMessage>
- {form.formState.errors?.note?.message}
- </FormErrorMessage>
- </FormControl>
</Flex>
{error && (
<Alert
@@ -427,36 +250,7 @@ export const UserDialog: FC<UserDialogProps> = () => {
</GridItem>
<GridItem>
- {services.length === 0 ? (
- <Spinner />
- ) :
- <FormControl
- isInvalid={
- !!form.formState.errors.services?.message
- }
- >
- <FormLabel>{t("userDialog.services")}</FormLabel>
- <Controller
- control={form.control}
- name="services"
- render={({ field }) => {
- return (
- <ServicesSelectList
- list={services}
- disabled={disabled}
- {...field}
- />
- );
- }}
- />
- <FormErrorMessage>
- {t(
- form.formState.errors.services
- ?.message as string
- )}
- </FormErrorMessage>
- </FormControl>
- }
+ <ServicesField t={t} services={services} form={form} disabled={disabled} />
</GridItem>
{isEditing && usageVisible && (
diff --git 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<User[]> => {
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;
})