Compare commits

...

36 Commits

Author SHA1 Message Date
7c38a99973 fix(ci): исправить путь container registry
All checks were successful
Build & Push Docker Image / build (push) Successful in 2m15s
- исправлен путь образа на gromov/casdoor
2026-04-11 19:42:25 +03:00
7c26dbb7d0 fix(ci): убрать тест версии из Dockerfile
Some checks failed
Build & Push Docker Image / build (push) Failing after 6m15s
- удалён TestGetVersionInfo, падает без .git в контейнере
2026-04-11 19:32:22 +03:00
61bc75b12e chore(ci): добавить ручной запуск workflow
Some checks failed
Build & Push Docker Image / build (push) Failing after 2m5s
- добавлен workflow_dispatch для ручного запуска сборки
2026-04-11 19:26:23 +03:00
18a8694d28 chore(ci): добавить Gitea Actions для сборки Docker-образа
- добавлен workflow сборки и пуша в container registry
- сборка при пуше в ветку custom
- target STANDARD (Alpine)
2026-04-11 19:19:01 +03:00
8478543c6b feat(i18n): добавить русский язык и конфигурацию разработки
- добавлен русский перевод интерфейса (web/src/locales/ru)
- восстановлен русский перевод бэкенда из Crowdin (i18n/locales/ru)
- добавлен ru в список языков организации
- добавлен Русский в селект языков
- добавлена конфигурация для локальной разработки (PostgreSQL, порт 5434)
- добавлен docker-compose.dev.yml
2026-04-11 19:11:15 +03:00
Yang Luo
25d8595e66 fix: improve top-left logo position 2026-04-11 22:33:20 +08:00
Yang Luo
3aafa91937 fix: improve hosting badge position and UI 2026-04-11 22:23:46 +08:00
Yang Luo
0077839549 fix: hide global scrollbar 2026-04-11 22:07:54 +08:00
Yang Luo
e1ee2ddee8 fix: add margin to 3 store pages 2026-04-11 22:02:05 +08:00
Yang Luo
b93be2d3e2 fix: add top breadcrumb bar 2026-04-11 21:53:57 +08:00
Yang Luo
77b56a2e40 fix: increase left sidebar width 2026-04-11 21:48:13 +08:00
Yang Luo
c0591f316e fix: increase org-select's width 2026-04-11 21:43:56 +08:00
Yang Luo
6749d46561 fix: improve top-left logo position 2026-04-11 21:42:21 +08:00
Yang Luo
a4a50f182b fix: hide left sidebar's scrollbar 2026-04-11 21:21:11 +08:00
Yang Luo
221d10a172 fix: fix Can't resolve 'rc-util/es/isEqual' bug 2026-04-11 20:46:34 +08:00
Yang Luo
5c051ba03d feat: improve table column width
BREAKING CHANGE: major release
2026-04-11 19:08:54 +08:00
Yang Luo
c16f4d2fb5 fix: improve xxx list page table's column row height 2026-04-11 19:01:28 +08:00
Yang Luo
fe185f880c fix: improve i18n keys 2026-04-11 19:00:45 +08:00
Yang Luo
b3bed1992b fix: improve "Loading" position 2026-04-11 18:54:05 +08:00
Yang Luo
be38d178fd fix: increase org-select's width 2026-04-11 18:50:19 +08:00
Yang Luo
3eb164e149 fix: add left margin to top-right user avatar 2026-04-11 18:46:47 +08:00
Yang Luo
6c3cd8a74b fix: set Sidebar menu: selected item - darker background 2026-04-11 18:43:10 +08:00
Yang Luo
c5ab4eec59 fix: fix top-left logo missing bug 2026-04-11 18:41:20 +08:00
Yang Luo
e8170884d7 fix: improve record and session list page UI 2026-04-11 18:40:22 +08:00
Yang Luo
729b21e8ae fix: use Apple Inter font 2026-04-11 18:32:36 +08:00
Yang Luo
bed67a1ff2 fix: improve top-left menu text 2026-04-11 18:21:29 +08:00
Yang Luo
df5f5def31 fix: improve list page's table title bar height 2026-04-11 18:10:29 +08:00
Yang Luo
76c56e9b2d fix: fix top-left menu highlight 2026-04-11 18:09:27 +08:00
Yang Luo
f46e229d5b fix: improve top-left logo 2026-04-11 17:50:25 +08:00
Yang Luo
112be9714b fix: reduce content area margin 2026-04-11 17:47:46 +08:00
Yang Luo
9d85362a24 fix: reduce top bar height 2026-04-11 17:43:03 +08:00
Yang Luo
37e2f13d99 feat: change to left sidebar 2026-04-11 17:32:13 +08:00
Yang Luo
f35398ea5c fix: use outlined icons in top navbar 2026-04-11 17:19:09 +08:00
Yang Luo
5a5470d5a3 fix: use shadcn theme by default 2026-04-11 17:15:35 +08:00
Yang Luo
948fc017e1 fix: improve i18n data 2026-04-11 17:01:26 +08:00
Yang Luo
c63184fc67 feat: upgrade to Antd 6.3.5 2026-04-11 16:53:23 +08:00
40 changed files with 3119 additions and 793 deletions

View File

@@ -0,0 +1,31 @@
name: Build & Push Docker Image
on:
push:
branches:
- custom
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: gromlab.ru
username: ${{ secrets.CR_USER }}
password: ${{ secrets.CR_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
target: STANDARD
push: true
tags: |
gromlab.ru/gromov/casdoor:latest
gromlab.ru/gromov/casdoor:${{ github.sha }}

View File

@@ -19,7 +19,6 @@ RUN go mod download
# Copy source files
COPY . .
RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go ./util/variable.go
RUN ./build.sh
FROM alpine:latest AS STANDARD

View File

@@ -1,43 +1,37 @@
appname = casdoor
httpport = 8000
runmode = dev
copyrequestbody = true
driverName = mysql
dataSourceName = root:123456@tcp(localhost:3306)/
dbName = casdoor
tableNamePrefix =
showSql = false
redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
socks5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10
initScore = 0
logPostOnly = true
isUsernameLowered = false
origin =
originFrontend =
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
showGithubCorner = false
forceLanguage = ""
defaultLanguage = "en"
aiAssistantUrl = "https://ai.casbin.com"
defaultApplication = "app-built-in"
maxItemsForFlatMenu = 7
enableErrorMask = false
enableGzip = true
inactiveTimeoutMinutes =
ldapServerPort = 389
ldapsCertId = ""
ldapsServerPort = 636
radiusServerPort = 1812
radiusDefaultOrganization = "built-in"
radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataNewOnly = false
initDataFile = "./init_data.json"
frontendBaseDir = "../cc_0"
appname = casdoor
httpport = 8000
runmode = dev
copyrequestbody = true
driverName = postgres
dataSourceName = "user=casdoor password=casdoor_dev host=localhost port=5434 sslmode=disable dbname=casdoor"
dbName = casdoor
tableNamePrefix =
showSql = false
redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
socks5Proxy = ""
verificationCodeTimeout = 10
initScore = 0
logPostOnly = true
isUsernameLowered = false
origin = "http://localhost:8000"
originFrontend = "http://localhost:7001"
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
showGithubCorner = false
forceLanguage = ""
defaultLanguage = "ru"
enableErrorMask = false
enableGzip = true
ldapServerPort = 389
ldapsServerPort = 636
radiusServerPort = 1812
radiusDefaultOrganization = "built-in"
radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataNewOnly = false
initDataFile = "./init_data.json"

43
conf/app.conf.orig Normal file
View File

@@ -0,0 +1,43 @@
appname = casdoor
httpport = 8000
runmode = dev
copyrequestbody = true
driverName = mysql
dataSourceName = root:123456@tcp(localhost:3306)/
dbName = casdoor
tableNamePrefix =
showSql = false
redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
socks5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10
initScore = 0
logPostOnly = true
isUsernameLowered = false
origin =
originFrontend =
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
showGithubCorner = false
forceLanguage = ""
defaultLanguage = "en"
aiAssistantUrl = "https://ai.casbin.com"
defaultApplication = "app-built-in"
maxItemsForFlatMenu = 7
enableErrorMask = false
enableGzip = true
inactiveTimeoutMinutes =
ldapServerPort = 389
ldapsCertId = ""
ldapsServerPort = 636
radiusServerPort = 1812
radiusDefaultOrganization = "built-in"
radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataNewOnly = false
initDataFile = "./init_data.json"
frontendBaseDir = "../cc_0"

37
conf/app.dev.conf Normal file
View File

@@ -0,0 +1,37 @@
appname = casdoor
httpport = 8000
runmode = dev
copyrequestbody = true
driverName = postgres
dataSourceName = "user=casdoor password=casdoor_dev host=localhost port=5434 sslmode=disable dbname=casdoor"
dbName = casdoor
tableNamePrefix =
showSql = false
redisEndpoint =
defaultStorageProvider =
isCloudIntranet = false
authState = "casdoor"
socks5Proxy = ""
verificationCodeTimeout = 10
initScore = 0
logPostOnly = true
isUsernameLowered = false
origin = "http://localhost:8000"
originFrontend = "http://localhost:7001"
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100
showGithubCorner = false
forceLanguage = ""
defaultLanguage = "ru"
enableErrorMask = false
enableGzip = true
ldapServerPort = 389
ldapsServerPort = 636
radiusServerPort = 1812
radiusDefaultOrganization = "built-in"
radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataNewOnly = false
initDataFile = "./init_data.json"

20
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
db:
image: postgres:16-alpine
restart: unless-stopped
ports:
- "5434:5432"
environment:
POSTGRES_USER: casdoor
POSTGRES_PASSWORD: casdoor_dev
POSTGRES_DB: casdoor
volumes:
- casdoor_pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U casdoor"]
interval: 5s
timeout: 5s
retries: 5
volumes:
casdoor_pg_data:

230
i18n/locales/ru/data.json Normal file
View File

@@ -0,0 +1,230 @@
{
"account": {
"Failed to add user": "Не удалось добавить пользователя",
"Get init score failed, error: %w": "Не удалось получить исходный балл, ошибка: %w",
"The application does not allow to sign up new account": "Приложение не позволяет зарегистрироваться новому аккаунту"
},
"auth": {
"Challenge method should be S256": "Метод проверки должен быть S256",
"DeviceCode Invalid": "Неверный код устройства",
"Failed to create user, user information is invalid: %s": "Не удалось создать пользователя, информация о пользователе недействительна: %s",
"Failed to login in: %s": "Не удалось войти в систему: %s",
"Invalid token": "Недействительный токен",
"State expected: %s, but got: %s": "Ожидался статус: %s, но получен: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Аккаунт провайдера: %s и имя пользователя: %s (%s) не существует и не может быть зарегистрирован через %s, пожалуйста, используйте другой способ регистрации",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Аккаунт для провайдера: %s и имя пользователя: %s (%s) не существует и не может быть зарегистрирован как новый аккаунт. Пожалуйста, обратитесь в службу поддержки IT",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Аккаунт поставщика: %s и имя пользователя: %s (%s) уже связаны с другим аккаунтом: %s (%s)",
"The application: %s does not exist": "Приложение: %s не существует",
"The application: %s has disabled users to signin": "Приложение: %s отключило вход пользователей",
"The group: %s does not exist": "Группа: %s не существует",
"The login method: login with LDAP is not enabled for the application": "Метод входа через LDAP отключен для этого приложения",
"The login method: login with SMS is not enabled for the application": "Метод входа через SMS отключен для этого приложения",
"The login method: login with email is not enabled for the application": "Метод входа через электронную почту отключен для этого приложения",
"The login method: login with face is not enabled for the application": "Метод входа через распознавание лица отключен для этого приложения",
"The login method: login with password is not enabled for the application": "Метод входа: вход с паролем не включен для приложения",
"The order: %s does not exist": "The order: %s does not exist",
"The organization: %s does not exist": "Организация: %s не существует",
"The organization: %s has disabled users to signin": "Организация: %s отключила вход пользователей",
"The plan: %s does not exist": "План: %s не существует",
"The pricing: %s does not exist": "Тариф: %s не существует",
"The pricing: %s does not have plan: %s": "Тариф: %s не имеет план: %s",
"The provider: %s does not exist": "Провайдер: %s не существует",
"The provider: %s is not enabled for the application": "Провайдер: %s не включен для приложения",
"Unauthorized operation": "Несанкционированная операция",
"Unknown authentication type (not password or provider), form = %s": "Неизвестный тип аутентификации (не пароль и не провайдер), форма = %s",
"User's tag: %s is not listed in the application's tags": "Тег пользователя: %s отсутствует в списке тегов приложения",
"UserCode Expired": "Срок действия кода пользователя истек",
"UserCode Invalid": "Неверный код пользователя",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "Платный пользователь %s не имеет активной или ожидающей подписки, а приложение %s не имеет цены по умолчанию",
"the application for user %s is not found": "Приложение для пользователя %s не найдено",
"the organization: %s is not found": "Организация: %s не найдена"
},
"cas": {
"Service %s and %s do not match": "Сервисы %s и %s не совпадают"
},
"check": {
"%s does not meet the CIDR format requirements: %s": "%s не соответствует требованиям формата CIDR: %s",
"Affiliation cannot be blank": "Принадлежность не может быть пустым значением",
"CIDR for IP: %s should not be empty": "CIDR для IP: %s не должен быть пустым",
"Default code does not match the code's matching rules": "Код по умолчанию не соответствует правилам соответствия кода",
"DisplayName cannot be blank": "Имя отображения не может быть пустым",
"DisplayName is not valid real name": "DisplayName не является действительным именем",
"Email already exists": "Электронная почта уже существует",
"Email cannot be empty": "Электронная почта не может быть пустой",
"Email is invalid": "Адрес электронной почты недействительный",
"Empty username.": "Пустое имя пользователя.",
"Face data does not exist, cannot log in": "Данные лица отсутствуют, вход невозможен",
"Face data mismatch": "Несоответствие данных лица",
"Failed to parse client IP: %s": "Не удалось разобрать IP клиента: %s",
"FirstName cannot be blank": "Имя не может быть пустым",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
"Invitation code cannot be blank": "Код приглашения не может быть пустым",
"Invitation code exhausted": "Код приглашения исчерпан",
"Invitation code is invalid": "Код приглашения недействителен",
"Invitation code suspended": "Код приглашения приостановлен",
"LastName cannot be blank": "Фамилия не может быть пустой",
"Multiple accounts with same uid, please check your ldap server": "Множественные учетные записи с тем же UID. Пожалуйста, проверьте свой сервер LDAP",
"Organization does not exist": "Организация не существует",
"Password cannot be empty": "Пароль не может быть пустым",
"Phone already exists": "Телефон уже существует",
"Phone cannot be empty": "Телефон не может быть пустым",
"Phone number is invalid": "Номер телефона является недействительным",
"Please register using the email corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя электронную почту, соответствующую коду приглашения",
"Please register using the phone corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя номер телефона, соответствующий коду приглашения",
"Please register using the username corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя имя пользователя, соответствующее коду приглашения",
"Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова",
"The invitation code has already been used": "Код приглашения уже использован",
"The password must contain at least one special character": "Пароль должен содержать хотя бы один специальный символ",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Пароль должен содержать хотя бы одну заглавную букву, одну строчную букву и одну цифру",
"The password must have at least 6 characters": "Пароль должен содержать не менее 6 символов",
"The password must have at least 8 characters": "Пароль должен содержать не менее 8 символов",
"The password must not contain any repeated characters": "Пароль не должен содержать повторяющихся символов",
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Пользователь был удален и не может быть использован для входа, пожалуйста, свяжитесь с администратором",
"The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору",
"The user: %s doesn't exist in LDAP server": "Пользователь: %s не существует на сервере LDAP",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Имя пользователя может состоять только из буквенно-цифровых символов, нижних подчеркиваний или дефисов, не может содержать последовательные дефисы или подчеркивания, а также не может начинаться или заканчиваться на дефис или подчеркивание.",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "The value \"%s\" for account field \"%s\" doesn't match the account item regex",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"",
"Username already exists": "Имя пользователя уже существует",
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
"Username is too long (maximum is 255 characters).": "Имя пользователя слишком длинное (максимальная длина - 255 символов).",
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
"Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.": "Имя пользователя поддерживает формат электронной почты. Также имя пользователя может содержать только буквенно-цифровые символы, подчеркивания или дефисы, не может иметь последовательных дефисов или подчеркиваний и не может начинаться или заканчиваться дефисом или подчеркиванием. Также обратите внимание на формат электронной почты.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
"Your IP address: %s has been banned according to the configuration of: ": "Ваш IP-адрес: %s заблокирован согласно конфигурации: ",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Срок действия вашего пароля истек. Пожалуйста, сбросьте пароль, нажав \"Забыли пароль\"",
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
"password or code is incorrect": "пароль или код неверны",
"password or code is incorrect, you have %s remaining chances": "Неправильный пароль или код, у вас осталось %s попыток",
"unsupported password type: %s": "неподдерживаемый тип пароля: %s"
},
"enforcer": {
"the adapter: %s is not found": "адаптер: %s не найден"
},
"general": {
"Failed to import groups": "Не удалось импортировать группы",
"Failed to import users": "Не удалось импортировать пользователей",
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
"Missing parameter": "Отсутствующий параметр",
"Only admin user can specify user": "Только администратор может указать пользователя",
"Please login first": "Пожалуйста, сначала войдите в систему",
"The LDAP: %s does not exist": "Группа LDAP: %s не существует",
"The organization: %s should have one application at least": "Организация: %s должна иметь хотя бы одно приложение",
"The syncer: %s does not exist": "The syncer: %s does not exist",
"The user: %s doesn't exist": "Пользователь %s не существует",
"The user: %s is not found": "The user: %s is not found",
"User is required for User category transaction": "User is required for User category transaction",
"Wrong userId": "Неверный идентификатор пользователя",
"don't support captchaProvider: ": "неподдерживаемый captchaProvider: ",
"this operation is not allowed in demo mode": "эта операция недоступна в демонстрационном режиме",
"this operation requires administrator to perform": "эта операция требует прав администратора"
},
"invitation": {
"Invitation %s does not exist": "Приглашение %s не существует"
},
"ldap": {
"Ldap server exist": "LDAP-сервер существует"
},
"link": {
"Please link first": "Пожалуйста, сначала установите ссылку",
"This application has no providers": "Это приложение не имеет провайдеров",
"This application has no providers of type": "Это приложение не имеет провайдеров данного типа",
"This provider can't be unlinked": "Этот провайдер не может быть отсоединен",
"You are not the global admin, you can't unlink other users": "Вы не являетесь глобальным администратором, вы не можете отсоединять других пользователей",
"You can't unlink yourself, you are not a member of any application": "Вы не можете отвязаться, так как вы не являетесь участником никакого приложения"
},
"organization": {
"Only admin can modify the %s.": "Только администратор может изменять %s.",
"The %s is immutable.": "%s неизменяемый.",
"Unknown modify rule %s.": "Неизвестное изменение правила %s.",
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "Добавление нового пользователя в организацию «built-in» (встроенная) в настоящее время отключено. Обратите внимание: все пользователи в организации «built-in» являются глобальными администраторами в Casdoor. См. документацию: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Если вы все еще хотите создать пользователя для организации «built-in», перейдите на страницу настроек организации и включите опцию «Имеет согласие на привилегии»."
},
"permission": {
"The permission: \"%s\" doesn't exist": "The permission: \"%s\" doesn't exist"
},
"product": {
"Product list cannot be empty": "Product list cannot be empty"
},
"provider": {
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
"Invalid application id": "Неверный идентификатор приложения",
"No ID Verification provider configured": "No ID Verification provider configured",
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
"the provider: %s does not exist": "Провайдер: %s не существует"
},
"resource": {
"User is nil for tag: avatar": "Пользователь равен нулю для тега: аватар",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Имя пользователя или полный путь к файлу пусты: имя_пользователя = %s, полный_путь_к_файлу = %s"
},
"saml": {
"Application %s not found": "Приложение %s не найдено"
},
"saml_sp": {
"provider %s's category is not SAML": "Категория провайдера %s не является SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Пустые параметры для emailForm: %v",
"Invalid Email receivers: %s": "Некорректные получатели электронной почты: %s",
"Invalid phone receivers: %s": "Некорректные получатели телефонных звонков: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "Объект «objectKey: %s» не разрешен",
"The provider type: %s is not supported": "Тип провайдера: %s не поддерживается"
},
"subscription": {
"Error": "Ошибка"
},
"ticket": {
"Ticket not found": "Ticket not found"
},
"token": {
"Grant_type: %s is not supported in this application": "Тип предоставления: %s не поддерживается в данном приложении",
"Invalid application or wrong clientSecret": "Недействительное приложение или неправильный clientSecret",
"Invalid client_id": "Недействительный идентификатор клиента",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "URI перенаправления: %s не существует в списке разрешенных URI перенаправления",
"Token not found, invalid accessToken": "Токен не найден, недействительный accessToken"
},
"user": {
"Display name cannot be empty": "Отображаемое имя не может быть пустым",
"ID card information and real name are required": "ID card information and real name are required",
"Identity verification failed": "Identity verification failed",
"MFA email is enabled but email is empty": "MFA по электронной почте включен, но электронная почта не указана",
"MFA phone is enabled but phone number is empty": "MFA по телефону включен, но номер телефона не указан",
"New password cannot contain blank space.": "Новый пароль не может содержать пробелы.",
"No application found for user": "No application found for user",
"The new password must be different from your current password": "Новый пароль должен отличаться от текущего пароля",
"User is already verified": "Пользователь уже подтвержден",
"the user's owner and name should not be empty": "владелец и имя пользователя не должны быть пустыми"
},
"util": {
"No application is found for userId: %s": "Не найдено заявки для пользователя с идентификатором: %s",
"No provider for category: %s is found for application: %s": "Нет провайдера для категории: %s для приложения: %s",
"The provider: %s is not found": "Поставщик: %s не найден"
},
"verification": {
"Invalid captcha provider.": "Недействительный поставщик CAPTCHA.",
"Phone number is invalid in your region %s": "Номер телефона недействителен в вашем регионе %s",
"The forgot password feature is disabled": "The forgot password feature is disabled",
"The verification code has already been used!": "Код подтверждения уже использован!",
"The verification code has not been sent yet!": "Код подтверждения еще не был отправлен!",
"Turing test failed.": "Тест Тьюринга не удался.",
"Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.",
"Unable to get the phone modify rule.": "Невозможно получить правило изменения телефона.",
"Unknown type": "Неизвестный тип",
"Wrong verification code!": "Неправильный код подтверждения!",
"You should verify your code in %d min!": "Вы должны проверить свой код через %d минут!",
"please add a SMS provider to the \"Providers\" list for the application: %s": "Пожалуйста, добавьте SMS-провайдера в список \"Провайдеры\" для приложения: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "пожалуйста, добавьте Email-провайдера в список \\\"Провайдеры\\\" для приложения: %s",
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"
},
"webauthn": {
"Found no credentials for this user": "Учетные данные для этого пользователя не найдены",
"Please call WebAuthnSigninBegin first": "Пожалуйста, сначала вызовите WebAuthnSigninBegin"
}
}

View File

@@ -54,6 +54,7 @@
"pt",
"tr",
"pl",
"ru",
"uk"
],
"masterPassword": "",

View File

@@ -134,7 +134,7 @@ func initBuiltInOrganization() bool {
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
UserTypes: []string{},
Tags: []string{},
Languages: []string{"en", "es", "fr", "de", "ja", "zh", "vi", "pt", "tr", "pl", "uk"},
Languages: []string{"en", "es", "fr", "de", "ja", "zh", "vi", "pt", "tr", "pl", "ru", "uk"},
InitScore: 2000,
AccountItems: getBuiltInAccountItems(),
EnableSoftDeletion: false,

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "casdoor",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -3,8 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/cssinjs": "^1.23.0",
"@ant-design/icons": "^5.6.1",
"@ant-design/cssinjs": "^2.1.2",
"@ant-design/icons": "^6.1.1",
"@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10",
"@ctrl/tinycolor": "^3.5.0",
@@ -24,8 +24,8 @@
"@web3-onboard/sequence": "^2.0.8",
"@web3-onboard/taho": "^2.0.5",
"@web3-onboard/trust": "^2.0.4",
"antd": "5.24.1",
"antd-token-previewer": "^2.0.8",
"antd": "6.3.5",
"antd-token-previewer": "^3.0.0",
"buffer": "^6.0.3",
"codemirror": "^6.0.1",
"cookie": "0.5.0",
@@ -107,6 +107,10 @@
"stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^28.0.0"
},
"resolutions": {
"@ant-design/cssinjs": "^2.1.2",
"rc-util": "^5.43.0"
},
"lint-staged": {
"src/**/*.{css,less}": [
"stylelint --fix"

View File

@@ -125,7 +125,7 @@ class AdapterListPage extends BaseListPage {
title: i18next.t("adapter:Use same DB"),
dataIndex: "useSameDb",
key: "useSameDb",
width: "120px",
width: "130px",
sorter: true,
render: (text, record, index) => {
return (
@@ -148,7 +148,7 @@ class AdapterListPage extends BaseListPage {
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "120px",
width: "140px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{

View File

@@ -23,6 +23,7 @@ import {Alert, Button, ConfigProvider, Drawer, FloatButton, Layout, Result, Tool
import {Route, Switch, withRouter} from "react-router-dom";
import CustomGithubCorner from "./common/CustomGithubCorner";
import * as Conf from "./Conf";
import {shadcnThemeComponents, shadcnThemeToken} from "./shadcnTheme";
import * as Auth from "./auth/Auth";
import EntryPage from "./EntryPage";
@@ -177,7 +178,7 @@ class App extends Component {
"/applications", "/providers", "/resources", "/certs", "/keys", // Identity
"/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
"/agents", "/servers", "/server-store", "/entries", "/sites", "/rules", // LLM AI
"/sessions", "/records", "/tokens", "/verifications", // Logging & Auditing
"/sessions", "/records", "/tokens", "/verifications", // Auditing
"/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
"/sysinfo", "/forms", "/syncers", "/webhooks", "/webhook-events", "/tickets", "/swagger", // Admin
];
@@ -614,9 +615,11 @@ class App extends Component {
locale={getAntdLocale(Setting.getLanguage())}
theme={{
token: {
...shadcnThemeToken,
colorPrimary: themeData.colorPrimary,
borderRadius: themeData.borderRadius,
},
components: shadcnThemeComponents,
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
<StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>
@@ -756,10 +759,12 @@ class App extends Component {
locale={getAntdLocale(Setting.getLanguage())}
theme={{
token: {
...shadcnThemeToken,
colorPrimary: this.state.themeData.colorPrimary,
colorInfo: this.state.themeData.colorPrimary,
borderRadius: this.state.themeData.borderRadius,
},
components: shadcnThemeComponents,
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
<StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>

View File

@@ -49,7 +49,7 @@ img {
justify-content: center;
border-radius: 5px;
width: 45px;
height: 64px;
height: 52px;
float: right;
cursor: pointer;
@@ -58,14 +58,34 @@ img {
}
}
.saas-hosting-btn {
font-weight: bold;
background-color: rgba(87, 52, 211, 0.4);
padding: 0 8px;
display: flex;
align-items: center;
height: 40px;
border-radius: 5px;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
background-color: rgba(87, 52, 211, 0.65);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(87, 52, 211, 0.35);
}
&:active {
transform: translateY(0);
box-shadow: none;
}
}
.org-select {
display: flex;
position: relative;
transform: translateY(50%);
margin: 0 10px !important;
float: right;
min-width: 120px;
max-width: 180px;
min-width: 200px;
max-width: 360px;
}
.rightDropDown {
@@ -85,6 +105,8 @@ img {
box-shadow: 0 1px 5px 0 rgb(51 51 51 / 14%);
flex: 1;
align-items: stretch;
margin: 0 3px 3px 3px !important;
border-radius: 4px !important;
}
.side-image {
@@ -151,3 +173,12 @@ img {
.ant-menu-horizontal {
border-bottom: none !important;
}
.ant-layout-sider-children {
display: flex;
flex-direction: column;
}
.ant-menu-inline {
flex: 1;
}

View File

@@ -27,8 +27,8 @@ export let StaticBaseUrl = "https://cdn.casbin.org";
export const InitThemeAlgorithm = true;
export const ThemeDefault = {
themeType: "default",
colorPrimary: "#5734d3",
borderRadius: 6,
colorPrimary: "#262626",
borderRadius: 10,
isCompact: false,
};

View File

@@ -164,7 +164,7 @@ class InvitationListPage extends BaseListPage {
title: i18next.t("invitation:Used count"),
dataIndex: "usedCount",
key: "usedCount",
width: "130px",
width: "140px",
sorter: true,
...this.getColumnSearchProps("usedCount"),
},

View File

@@ -14,17 +14,19 @@
import * as Setting from "./Setting";
import {Avatar, Button, Card, Drawer, Dropdown, Menu, Result, Tooltip} from "antd";
import Sider from "antd/es/layout/Sider";
import EnableMfaNotification from "./common/notifaction/EnableMfaNotification";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import React, {useState} from "react";
import React, {useEffect, useState} from "react";
import i18next from "i18next";
import {
AppstoreTwoTone,
BarsOutlined, CheckCircleTwoTone, DeploymentUnitOutlined, DollarTwoTone, DownOutlined,
HomeTwoTone,
LockTwoTone, LogoutOutlined,
SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone,
WalletTwoTone
AppstoreOutlined,
BarsOutlined, CheckCircleOutlined, DeploymentUnitOutlined, DollarOutlined, DownOutlined,
HomeOutlined,
LockOutlined, LogoutOutlined,
MenuFoldOutlined, MenuUnfoldOutlined,
SafetyCertificateOutlined, SettingOutlined,
WalletOutlined
} from "@ant-design/icons";
import Dashboard from "./basic/Dashboard";
import AppListPage from "./basic/AppListPage";
@@ -97,6 +99,7 @@ import ThemeSelect from "./common/select/ThemeSelect";
import OpenTour from "./common/OpenTour";
import OrganizationSelect from "./common/select/OrganizationSelect";
import AccountAvatar from "./account/AccountAvatar";
import BreadcrumbBar from "./common/BreadcrumbBar";
import {Content, Header} from "antd/es/layout/layout";
import * as AuthBackend from "./auth/AuthBackend";
import {clearWeb3AuthToken} from "./auth/Web3Auth";
@@ -119,11 +122,57 @@ import SiteEditPage from "./SiteEditPage";
import RuleListPage from "./RuleListPage";
import RuleEditPage from "./RuleEditPage";
function getMenuParentKey(uri) {
if (!uri) {return null;}
if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) {return "/home";}
if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {return "/orgs";}
if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs") || uri.includes("/keys")) {return "/identity";}
if (uri.includes("/agents") || uri.includes("/servers") || uri.includes("/entries") || uri.includes("/sites") || uri.includes("/rules")) {return "/gateway";}
if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {return "/auth";}
if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {return "/logs";}
if (uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions") || uri.includes("/cart")) {return "/business";}
if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/tickets")) {return "/admin";}
return null;
}
function ManagementPage(props) {
const [menuVisible, setMenuVisible] = useState(false);
const [siderCollapsed, setSiderCollapsed] = useState(() => localStorage.getItem("siderCollapsed") === "true");
const [menuOpenKeys, setMenuOpenKeys] = useState(() => {
const parentKey = getMenuParentKey(props.uri || location.pathname);
return parentKey ? [parentKey] : [];
});
useEffect(() => {
const parentKey = getMenuParentKey(props.uri);
if (parentKey) {
setMenuOpenKeys(prev =>
prev.includes(parentKey) ? prev : [...prev, parentKey]
);
}
}, [props.uri]);
const organization = props.account?.organization;
const navItems = Setting.isLocalAdminUser(props.account) ? organization?.navItems : (organization?.userNavItems ?? []);
const widgetItems = organization?.widgetItems;
const currentUri = props.uri || location.pathname;
const selectedLeafKey = "/" + (currentUri.split("/").filter(Boolean)[0] || "");
const isDark = props.themeAlgorithm.includes("dark");
const textColor = isDark ? "white" : "black";
const siderLogo = (() => {
if (!props.account?.organization) {return Setting.getLogo(props.themeAlgorithm);}
let logo = props.account.organization.logo || Setting.getLogo(props.themeAlgorithm);
if (isDark && props.account.organization.logoDark) {
logo = props.account.organization.logoDark;
}
return logo;
})();
const toggleSider = () => {
const next = !siderCollapsed;
setSiderCollapsed(next);
localStorage.setItem("siderCollapsed", String(next));
};
function logout() {
AuthBackend.logout()
@@ -150,13 +199,13 @@ function ManagementPage(props) {
function renderAvatar() {
if (props.account.avatar === "") {
return (
<Avatar style={{backgroundColor: Setting.getAvatarColor(props.account.name), verticalAlign: "middle"}} size="large">
<Avatar style={{backgroundColor: Setting.getAvatarColor(props.account.name), verticalAlign: "middle", marginLeft: 8}} size="large">
{Setting.getShortName(props.account.name)}
</Avatar>
);
} else {
return (
<Avatar src={props.account.avatar} style={{verticalAlign: "middle"}} size="large"
<Avatar src={props.account.avatar} style={{verticalAlign: "middle", marginLeft: 8}} size="large"
icon={<AccountAvatar src={props.account.avatar} style={{verticalAlign: "middle"}} size={40} />}
>
{Setting.getShortName(props.account.name)}
@@ -235,7 +284,7 @@ function ManagementPage(props) {
Setting.getItem(<ThemeSelect themeAlgorithm={props.themeAlgorithm} onChange={props.setLogoAndThemeAlgorithm} />, "theme"),
Setting.getItem(<LanguageSelect languages={props.account.organization.languages} />, "language"),
Setting.getItem(Conf.AiAssistantUrl?.trim() && (
<Tooltip title="Click to open AI assistant">
<Tooltip title={i18next.t("general:Click to open AI assistant")}>
<div className="select-box" onClick={props.openAiAssistant}>
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
</div>
@@ -245,10 +294,10 @@ function ManagementPage(props) {
];
if (widgetItemsIsAll()) {
return widgets.map(item => item.label);
return widgets.reverse().map(item => item.label);
}
return widgets.filter(item => widgetItems.includes(item.key)).map(item => item.label);
return widgets.filter(item => widgetItems.includes(item.key)).reverse().map(item => item.label);
}
function renderAccountMenu() {
@@ -263,8 +312,13 @@ function ManagementPage(props) {
} else {
return (
<React.Fragment>
{renderRightDropdown()}
{renderWidgets()}
{Setting.isLocalAdminUser(props.account) && Conf.ShowGithubCorner && !Setting.isMobile() &&
<a href={"https://casdoor.com"} target="_blank" rel="noreferrer" style={{marginRight: "8px"}}>
<span className="saas-hosting-btn">
🚀 SaaS Hosting 🔥
</span>
</a>
}
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect
initValue={Setting.getOrganization()}
@@ -276,6 +330,8 @@ function ManagementPage(props) {
}}
/>
}
{renderWidgets()}
{renderRightDropdown()}
</React.Fragment>
);
}
@@ -288,51 +344,20 @@ function ManagementPage(props) {
return [];
}
let textColor = "black";
const twoToneColor = props.themeData.colorPrimary;
let logo = props.account.organization.logo ? props.account.organization.logo : Setting.getLogo(props.themeAlgorithm);
if (props.themeAlgorithm.includes("dark")) {
if (props.account.organization.logoDark) {
logo = props.account.organization.logoDark;
}
textColor = "white";
}
!Setting.isMobile() ? res.push({
label:
<Link to="/">
<img className="logo" src={logo ?? props.logo} alt="logo" />
</Link>,
disabled: true, key: "logo",
style: {
padding: 0,
height: "auto",
},
}) : null;
res.push(Setting.getItem(<Link style={{color: textColor}} to="/">{i18next.t("general:Home")}</Link>, "/home", <HomeTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/">{i18next.t("general:Home")}</Link>, "/home", <HomeOutlined />, [
Setting.getItem(<Link to="/">{i18next.t("general:Dashboard")}</Link>, "/"),
Setting.getItem(<Link to="/shortcuts">{i18next.t("general:Shortcuts")}</Link>, "/shortcuts"),
Setting.getItem(<Link to="/apps">{i18next.t("general:Apps")}</Link>, "/apps"),
]));
if (Setting.isLocalAdminUser(props.account) && Conf.ShowGithubCorner) {
res.push(Setting.getItem(<a href={"https://casdoor.com"}>
<span style={{fontWeight: "bold", backgroundColor: "rgba(87,52,211,0.4)", marginTop: "12px", paddingLeft: "5px", paddingRight: "5px", display: "flex", alignItems: "center", height: "40px", borderRadius: "5px"}}>
🚀 SaaS Hosting 🔥
</span>
</a>, "#"));
}
res.push(Setting.getItem(<Link style={{color: textColor}} to="/organizations">{i18next.t("general:User Management")}</Link>, "/orgs", <AppstoreTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/organizations">{i18next.t("general:User Management")}</Link>, "/orgs", <AppstoreOutlined />, [
Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>, "/organizations"),
Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>, "/groups"),
Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>, "/users"),
Setting.getItem(<Link to="/invitations">{i18next.t("general:Invitations")}</Link>, "/invitations"),
]));
res.push(Setting.getItem(<Link style={{color: textColor}} to="/applications">{i18next.t("general:Identity")}</Link>, "/identity", <LockTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/applications">{i18next.t("general:Identity")}</Link>, "/identity", <LockOutlined />, [
Setting.getItem(<Link to="/applications">{i18next.t("general:Applications")}</Link>, "/applications"),
Setting.getItem(<Link to="/providers">{i18next.t("application:Providers")}</Link>, "/providers"),
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
@@ -340,7 +365,7 @@ function ManagementPage(props) {
Setting.getItem(<Link to="/keys">{i18next.t("general:Keys")}</Link>, "/keys"),
]));
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateOutlined />, [
Setting.getItem(<Link to="/roles">{i18next.t("general:Roles")}</Link>, "/roles"),
Setting.getItem(<Link to="/permissions">{i18next.t("general:Permissions")}</Link>, "/permissions"),
Setting.getItem(<Link to="/models">{i18next.t("general:Models")}</Link>, "/models"),
@@ -354,7 +379,7 @@ function ManagementPage(props) {
}
})));
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sites">{i18next.t("general:LLM AI")}</Link>, "/gateway", <CheckCircleTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sites">{i18next.t("general:LLM AI")}</Link>, "/gateway", <CheckCircleOutlined />, [
Setting.getItem(<Link to="/agents">{i18next.t("general:Agents")}</Link>, "/agents"),
Setting.getItem(<Link to="/servers">{i18next.t("general:MCP Servers")}</Link>, "/servers"),
Setting.getItem(<Link to="/server-store">{i18next.t("general:MCP Store")}</Link>, "/server-store"),
@@ -363,14 +388,14 @@ function ManagementPage(props) {
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
]));
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sessions">{i18next.t("general:Auditing")}</Link>, "/logs", <WalletOutlined />, [
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
Setting.getItem(<Link to="/records">{i18next.t("general:Records")}</Link>, "/records"),
Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>, "/tokens"),
Setting.getItem(<Link to="/verifications">{i18next.t("general:Verifications")}</Link>, "/verifications"),
]));
res.push(Setting.getItem(<Link style={{color: textColor}} to="/products">{i18next.t("general:Business & Payments")}</Link>, "/business", <DollarTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/products">{i18next.t("general:Business")}</Link>, "/business", <DollarOutlined />, [
Setting.getItem(<Link to="/product-store">{i18next.t("general:Product Store")}</Link>, "/product-store"),
Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>, "/products"),
Setting.getItem(<Link to="/cart">{i18next.t("general:Cart")}</Link>, "/cart"),
@@ -383,7 +408,7 @@ function ManagementPage(props) {
]));
if (Setting.isAdminUser(props.account)) {
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingOutlined />, [
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
@@ -392,7 +417,7 @@ function ManagementPage(props) {
Setting.getItem(<Link to="/tickets">{i18next.t("general:Tickets")}</Link>, "/tickets"),
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
} else {
res.push(Setting.getItem(<Link style={{color: textColor}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
res.push(Setting.getItem(<Link style={{color: textColor}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingOutlined />, [
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
@@ -580,52 +605,113 @@ function ManagementPage(props) {
setMenuVisible(true);
};
const siderWidth = 256;
const siderCollapsedWidth = 80;
const showSider = !Setting.isMobile() && !props.requiredEnableMfa;
const contentMarginLeft = showSider ? (siderCollapsed ? siderCollapsedWidth : siderWidth) : 0;
return (
<React.Fragment>
<EnableMfaNotification account={props.account} />
<Header style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0", marginBottom: "4px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
{
props.requiredEnableMfa || (Setting.isMobile() ? (
<React.Fragment>
<Drawer title={i18next.t("general:Close")} placement="left" open={menuVisible} onClose={onClose}>
<Menu
items={getMenuItems()}
mode={"inline"}
selectedKeys={[props.selectedMenuKey]}
style={{lineHeight: "64px"}}
onClick={onClose}
>
</Menu>
</Drawer>
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
{i18next.t("general:Menu")}
</Button>
</React.Fragment>
) : (
// Padding 1px for Menu Item Highlight border
<div style={{flex: 1, overflow: "hidden", paddingBottom: "1px"}}>
<Menu
onClick={onClose}
items={getMenuItems()}
mode={"horizontal"}
selectedKeys={[props.selectedMenuKey]}
style={{backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
{showSider && (
<Sider
collapsed={siderCollapsed}
collapsedWidth={siderCollapsedWidth}
width={siderWidth}
trigger={null}
theme={isDark ? "dark" : "light"}
style={{
height: "100vh",
position: "fixed",
left: 0,
top: 0,
bottom: 0,
zIndex: 100,
boxShadow: "2px 0 8px rgba(0,0,0,0.08)",
display: "flex",
flexDirection: "column",
}}
>
<div style={{
height: 52,
flexShrink: 0,
display: "flex",
alignItems: "center",
justifyContent: siderCollapsed ? "center" : "flex-start",
padding: siderCollapsed ? "0" : "0 16px 0 24px",
overflow: "hidden",
}}>
<Link to="/">
<img
src={siderCollapsed ? (organization?.favicon || siderLogo || props.logo) : (siderLogo ?? props.logo)}
alt="logo"
style={{
height: siderCollapsed ? 28 : 40,
width: siderCollapsed ? 28 : undefined,
maxWidth: siderCollapsed ? 28 : 160,
objectFit: "contain",
borderRadius: siderCollapsed ? 4 : 0,
transition: "max-width 0.2s, height 0.2s, width 0.2s",
}}
/>
</div>
))
}
<div style={{flexShrink: 0}}>
{renderAccountMenu()}
</div>
</Header>
<Content style={{display: "flex", flexDirection: "column"}} >
{isWithoutCard() ?
renderRouter() :
<Card className="content-warp-card">
{renderRouter()}
</Card>
}
</Content>
</Link>
</div>
<div className="sider-menu-container" style={{flex: 1, overflow: "auto"}}>
<Menu
mode="inline"
items={getMenuItems()}
selectedKeys={[selectedLeafKey]}
openKeys={menuOpenKeys}
onOpenChange={setMenuOpenKeys}
theme={isDark ? "dark" : "light"}
style={{borderRight: 0}}
/>
</div>
</Sider>
)}
<div style={{marginLeft: contentMarginLeft, transition: "margin-left 0.2s", display: "flex", flexDirection: "column", minHeight: "100vh"}}>
<Header style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0", marginBottom: "4px", backgroundColor: isDark ? "black" : "white", position: "sticky", top: 0, zIndex: 99, boxShadow: "0 1px 4px rgba(0,0,0,0.08)", height: "52px", lineHeight: "52px"}}>
<div style={{display: "flex", alignItems: "center"}}>
{props.requiredEnableMfa ? null : (Setting.isMobile() ? (
<React.Fragment>
<Drawer title={i18next.t("general:Close")} placement="left" open={menuVisible} onClose={onClose}>
<Menu
items={getMenuItems()}
mode={"inline"}
selectedKeys={[selectedLeafKey]}
openKeys={menuOpenKeys}
onOpenChange={setMenuOpenKeys}
style={{lineHeight: "48px"}}
onClick={onClose}
/>
</Drawer>
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
{i18next.t("general:Menu")}
</Button>
</React.Fragment>
) : (
<Button
icon={siderCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={toggleSider}
type="text"
style={{fontSize: 16, width: 40, height: 40}}
/>
))}
<BreadcrumbBar uri={currentUri} />
</div>
<div style={{flexShrink: 0, display: "flex", alignItems: "center"}}>
{renderAccountMenu()}
</div>
</Header>
<Content style={{display: "flex", flexDirection: "column"}}>
{isWithoutCard() ?
renderRouter() :
<Card className="content-warp-card" styles={{body: {padding: 0, margin: 0}}}>
{renderRouter()}
</Card>
}
</Content>
</div>
</React.Fragment>
);
}

View File

@@ -223,7 +223,7 @@ class OrganizationListPage extends BaseListPage {
title: i18next.t("general:Password type"),
dataIndex: "passwordType",
key: "passwordType",
width: "150px",
width: "160px",
sorter: true,
filterMultiple: false,
filters: [
@@ -267,7 +267,7 @@ class OrganizationListPage extends BaseListPage {
title: i18next.t("organization:User balance"),
dataIndex: "userBalance",
key: "userBalance",
width: "120px",
width: "130px",
sorter: true,
render: (text, record, index) => {
return text ?? 0;
@@ -277,7 +277,7 @@ class OrganizationListPage extends BaseListPage {
title: i18next.t("organization:Balance credit"),
dataIndex: "balanceCredit",
key: "balanceCredit",
width: "120px",
width: "130px",
sorter: true,
render: (text, record, index) => {
return text ?? 0;
@@ -287,7 +287,7 @@ class OrganizationListPage extends BaseListPage {
title: i18next.t("organization:Balance currency"),
dataIndex: "balanceCurrency",
key: "balanceCurrency",
width: "140px",
width: "160px",
sorter: true,
render: (text, record, index) => {
return text || "USD";

View File

@@ -415,7 +415,7 @@ class PermissionListPage extends BaseListPage {
dataIndex: "approveTime",
key: "approveTime",
filterMultiple: false,
width: "120px",
width: "130px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);

View File

@@ -341,7 +341,7 @@ class ProductStorePage extends React.Component {
render() {
return (
<div>
<div style={{padding: "16px"}}>
<FloatingCartButton
itemCount={this.state.cartItemCount}
onClick={() => this.props.history.push("/cart")}

View File

@@ -72,7 +72,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "110px",
width: "140px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
@@ -102,7 +102,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Method"),
dataIndex: "method",
key: "method",
width: "100px",
width: "110px",
sorter: true,
filterMultiple: false,
filters: [
@@ -129,7 +129,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("user:Language"),
dataIndex: "language",
key: "language",
width: "90px",
width: "120px",
sorter: true,
...this.getColumnSearchProps("language"),
},
@@ -204,13 +204,13 @@ class RecordListPage extends BaseListPage {
sorter: true,
fixed: "right",
render: (text, record, index) => (
<Button type="link" onClick={() => {
<Button onClick={() => {
this.setState({
detailRecord: record,
detailShow: true,
});
}}>
{i18next.t("general:Detail")}
{i18next.t("general:View")}
</Button>
),
},

View File

@@ -215,7 +215,7 @@ class ServerStorePage extends React.Component {
const filteredServers = this.getFilteredOnlineServers();
return (
<div>
<div style={{padding: "16px"}}>
<div style={{display: "flex", gap: "8px", marginBottom: "12px"}}>
<Input
allowClear

View File

@@ -140,6 +140,7 @@ class SessionListPage extends BaseListPage {
const paginationProps = {
total: this.state.pagination.total,
pageSize: this.state.pagination.pageSize,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
@@ -148,6 +149,11 @@ class SessionListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={sessions} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Sessions")}&nbsp;&nbsp;&nbsp;&nbsp;
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>

View File

@@ -48,6 +48,7 @@ export const Countries = [
{label: "Português", key: "pt", country: "PT", alt: "Português"},
{label: "Türkçe", key: "tr", country: "TR", alt: "Türkçe"},
{label: "Polski", key: "pl", country: "PL", alt: "Polski"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
{label: "Українська", key: "uk", country: "UA", alt: "Українська"},
];

View File

@@ -197,7 +197,7 @@ class SiteListPage extends BaseListPage {
title: i18next.t("site:Other domains"),
dataIndex: "otherDomains",
key: "otherDomains",
width: "120px",
width: "140px",
sorter: (a, b) => a.otherDomains.localeCompare(b.otherDomains),
render: (text, record, index) => {
return record.otherDomains.map(domain => {

View File

@@ -162,7 +162,7 @@ class SyncerListPage extends BaseListPage {
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "130px",
width: "140px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{
@@ -215,7 +215,7 @@ class SyncerListPage extends BaseListPage {
title: i18next.t("syncer:Sync interval"),
dataIndex: "syncInterval",
key: "syncInterval",
width: "140px",
width: "150px",
sorter: true,
...this.getColumnSearchProps("syncInterval"),
},

View File

@@ -109,7 +109,7 @@ class TokenListPage extends BaseListPage {
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
width: "120px",
width: "130px",
sorter: true,
...this.getColumnSearchProps("application"),
render: (text, record, index) => {
@@ -124,7 +124,7 @@ class TokenListPage extends BaseListPage {
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
width: "140px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {

View File

@@ -396,7 +396,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("application:Real name"),
dataIndex: "realName",
key: "realName",
width: "120px",
width: "130px",
sorter: true,
...this.getColumnSearchProps("realName"),
},
@@ -464,7 +464,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("user:Register source"),
dataIndex: "registerSource",
key: "registerSource",
width: "150px",
width: "160px",
sorter: true,
...this.getColumnSearchProps("registerSource"),
},
@@ -482,7 +482,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("organization:Balance credit"),
dataIndex: "balanceCredit",
key: "balanceCredit",
width: "120px",
width: "130px",
sorter: true,
render: (text, record, index) => {
return text ?? 0;
@@ -492,7 +492,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("organization:Balance currency"),
dataIndex: "balanceCurrency",
key: "balanceCurrency",
width: "140px",
width: "160px",
sorter: true,
render: (text, record, index) => {
return text || "USD";
@@ -514,7 +514,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("user:Is forbidden"),
dataIndex: "isForbidden",
key: "isForbidden",
width: "110px",
width: "120px",
sorter: true,
render: (text, record, index) => {
return (
@@ -545,9 +545,9 @@ class UserListPage extends BaseListPage {
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name) || (record.owner === "built-in" && record.name === "admin");
return (
<Space>
<Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
<Button size={isTreePage ? "small" : "middle"} onClick={() => {
this.impersonateUser(`${record.owner}/${record.name}`);
}}>{i18next.t("general:Impersonation")}
}}>{i18next.t("general:Impersonate")}
</Button>
<Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
sessionStorage.setItem("userListUrl", window.location.pathname);

View File

@@ -142,7 +142,7 @@ class VerificationListPage extends BaseListPage {
title: i18next.t("login:Verification code"),
dataIndex: "code",
key: "code",
width: "150px",
width: "160px",
sorter: true,
...this.getColumnSearchProps("code"),
},

View File

@@ -148,7 +148,7 @@ class WebhookListPage extends BaseListPage {
title: i18next.t("webhook:Content type"),
dataIndex: "contentType",
key: "contentType",
width: "140px",
width: "150px",
sorter: true,
filterMultiple: false,
filters: [
@@ -171,7 +171,7 @@ class WebhookListPage extends BaseListPage {
title: i18next.t("webhook:Is user extended"),
dataIndex: "isUserExtended",
key: "isUserExtended",
width: "140px",
width: "150px",
sorter: true,
render: (text, record, index) => {
return (

View File

@@ -122,8 +122,8 @@ const Dashboard = (props) => {
}
}
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "10%"}} />
<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "calc(100vh - 120px)"}}>
<Spin size="large" tip={i18next.t("login:Loading")} />
</div>
);
}

View File

@@ -20,7 +20,7 @@ const ShortcutsPage = () => {
};
return (
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center", padding: "16px"}}>
<GridCards items={getItems()} />
</div>
);

View File

@@ -0,0 +1,109 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Breadcrumb} from "antd";
import {Link} from "react-router-dom";
import i18next from "i18next";
const RESOURCE_LABELS = {
"apps": "general:Apps",
"shortcuts": "general:Shortcuts",
"account": "account:My Account",
"organizations": "general:Organizations",
"users": "general:Users",
"groups": "general:Groups",
"trees": "general:Groups",
"invitations": "general:Invitations",
"applications": "general:Applications",
"providers": "application:Providers",
"resources": "general:Resources",
"certs": "general:Certs",
"keys": "general:Keys",
"agents": "general:Agents",
"servers": "general:MCP Servers",
"server-store": "general:MCP Store",
"entries": "general:Entries",
"sites": "general:Sites",
"rules": "general:Rules",
"roles": "general:Roles",
"permissions": "general:Permissions",
"models": "general:Models",
"adapters": "general:Adapters",
"enforcers": "general:Enforcers",
"sessions": "general:Sessions",
"records": "general:Records",
"tokens": "general:Tokens",
"verifications": "general:Verifications",
"product-store": "general:Product Store",
"products": "general:Products",
"cart": "general:Cart",
"orders": "general:Orders",
"payments": "general:Payments",
"plans": "general:Plans",
"pricings": "general:Pricings",
"subscriptions": "general:Subscriptions",
"transactions": "general:Transactions",
"sysinfo": "general:System Info",
"forms": "general:Forms",
"syncers": "general:Syncers",
"webhooks": "general:Webhooks",
"webhook-events": "general:Webhook Events",
"tickets": "general:Tickets",
"ldap": "general:LDAP",
"mfa": "general:MFA",
};
function buildBreadcrumbItems(uri) {
const pathSegments = (uri || "").split("/").filter(Boolean);
const homeItem = {title: <Link to="/">{i18next.t("general:Home")}</Link>};
if (pathSegments.length === 0) {
return null;
}
const rootSegment = pathSegments[0];
const listLabelKey = RESOURCE_LABELS[rootSegment];
if (!listLabelKey) {
return null;
}
if (pathSegments.length === 1) {
return [
homeItem,
{title: i18next.t(listLabelKey)},
];
}
const lastSegment = pathSegments[pathSegments.length - 1];
const lastLabelKey = RESOURCE_LABELS[lastSegment];
const lastLabel = lastLabelKey ? i18next.t(lastLabelKey) : lastSegment;
return [
homeItem,
{title: <Link to={`/${rootSegment}`}>{i18next.t(listLabelKey)}</Link>},
{title: lastLabel},
];
}
const BreadcrumbBar = ({uri}) => {
const items = buildBreadcrumbItems(uri);
if (!items) {
return null;
}
return <Breadcrumb items={items} style={{marginLeft: 8}} />;
};
export default BreadcrumbBar;

View File

@@ -61,7 +61,7 @@ export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck
],
},
{
title: i18next.t("general:Logging & Auditing"),
title: i18next.t("general:Auditing"),
key: "/sessions-top",
children: [
{title: i18next.t("general:Sessions"), key: "/sessions"},
@@ -71,7 +71,7 @@ export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck
],
},
{
title: i18next.t("general:Business & Payments"),
title: i18next.t("general:Business"),
key: "/business-top",
children: [
{title: i18next.t("general:Products"), key: "/products"},

View File

@@ -15,6 +15,7 @@
import React from "react";
import {Tooltip} from "antd";
import {QuestionCircleOutlined} from "@ant-design/icons";
import i18next from "i18next";
import * as TourConfig from "../TourConfig";
import * as Setting from "../Setting";
@@ -34,7 +35,7 @@ class OpenTour extends React.Component {
render() {
return (
this.canTour() ?
<Tooltip title="Click to open tour">
<Tooltip title={i18next.t("general:Click to open tour")}>
<div className="select-box" style={{display: Setting.isMobile() ? "none" : null, ...this.props.style}} onClick={() => TourConfig.setIsTourVisible(true)} >
<QuestionCircleOutlined style={{fontSize: "24px"}} />
</div>

View File

@@ -1,6 +1,18 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
html {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
html::-webkit-scrollbar {
display: none; /* Chrome/Safari/WebKit */
}
body {
margin: 0;
font-family:
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
@@ -32,13 +44,19 @@ code {
float: left;
}
.ant-table.ant-table-middle .ant-table-title,
.ant-table.ant-table-middle .ant-table-footer,
.ant-table.ant-table-middle thead > tr > th,
.ant-table.ant-table-middle tbody > tr > td {
.ant-table.ant-table-medium .ant-table-title {
padding: 5px 8px !important;
}
.ant-table.ant-table-medium .ant-table-footer,
.ant-table.ant-table-medium .ant-table-cell {
padding: 1px 8px !important;
}
.ant-table.ant-table-medium .ant-table-thead .ant-table-cell {
padding: 10px 8px !important;
}
.ant-list-sm .ant-list-item {
padding: 2px !important;
}
@@ -67,3 +85,83 @@ code {
.no-horizontal-scroll-editor [class*="CodeMirror-scroll"] {
overflow-x: hidden !important;
}
/* Bold overrides for key UI elements */
/* Sidebar menu: item labels */
.ant-menu .ant-menu-item,
.ant-menu .ant-menu-item a,
.ant-menu .ant-menu-submenu-title {
font-weight: 600 !important;
}
/* Sidebar menu: group titles */
.ant-menu .ant-menu-item-group-title {
font-weight: 700 !important;
letter-spacing: 0.04em;
}
/* Table: column headers (thead) */
.ant-table .ant-table-thead > tr > th,
.ant-table .ant-table-thead > tr > td {
font-weight: 600 !important;
}
/* Table: footer */
.ant-table .ant-table-footer {
font-weight: 600 !important;
}
/* Table: title bar */
.ant-table .ant-table-title {
font-weight: 700 !important;
}
/* Card: header title */
.ant-card .ant-card-head-title {
font-weight: 600 !important;
}
/* Descriptions: item labels */
.ant-descriptions .ant-descriptions-item-label {
font-weight: 600 !important;
}
/* Tabs: tab button labels */
.ant-tabs .ant-tabs-tab .ant-tabs-tab-btn {
font-weight: 600 !important;
}
/* Layout sider: menu item and submenu labels */
.ant-layout-sider .ant-menu-item,
.ant-layout-sider .ant-menu-submenu-title {
font-weight: 600 !important;
}
/* Table loading spinner - give the overlay enough height so spinner appears centered (antd v6) */
.ant-table-wrapper .ant-spin.ant-spin-spinning {
min-height: 420px;
}
/* Sidebar menu: selected item - darker background */
.ant-layout-sider .ant-menu-light .ant-menu-item-selected,
.ant-layout-sider .ant-menu-light>.ant-menu .ant-menu-item-selected {
background-color: rgba(0, 0, 0, 0.15) !important;
}
/* Sidebar menu container: hide scrollbar but keep scroll behavior */
.sider-menu-container {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.sider-menu-container::-webkit-scrollbar {
display: none; /* Chrome/Safari/WebKit */
}
/* Message popups: static API renders outside ConfigProvider, override font explicitly */
.ant-message,
.ant-message .ant-message-notice-content,
.ant-message .ant-message-custom-content,
.ant-message .ant-message-custom-content span {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif !important;
}

1515
web/src/locales/ru/data.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -284,8 +284,8 @@
"Cancel": "取消",
"Captcha": "人机验证码",
"Cart": "购物车",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Category": "类别",
"Category - Tooltip": "所属的类别",
"Cert": "证书",
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
"Certs": "证书",
@@ -347,14 +347,14 @@
"Failed to upload": "上传失败",
"Failed to verify": "验证失败",
"False": "假",
"Favicon": "组织Favicon",
"Favicon - Tooltip": "该组织所有Casdoor页面中所使用的Favicon图标URL",
"Favicon": "图标",
"Favicon - Tooltip": "该组织所有Casdoor页面中所使用的Favicon图标链接",
"Filter": "筛选",
"First name": "名字",
"First name - Tooltip": "用户的名字",
"Forced redirect origin - Tooltip": "强制重定向到指定的来源",
"Forget URL": "忘记密码URL",
"Forget URL - Tooltip": "自定义忘记密码页面的URL不设置时采用Casdoor默认的忘记密码页面设置后Casdoor各类页面的忘记密码链接会跳转到该URL",
"Forget URL": "忘记密码链接",
"Forget URL - Tooltip": "自定义忘记密码页面的链接不设置时采用Casdoor默认的忘记密码页面设置后Casdoor各类页面的忘记密码链接会跳转到该URL",
"Forms": "表单",
"Found some texts still not translated? Please help us translate at": "发现有些文字尚未翻译?请移步这里帮我们翻译:",
"Generate": "生成",

168
web/src/shadcnTheme.js Normal file
View File

@@ -0,0 +1,168 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Shadcn-style Ant Design theme configuration.
// Adapted from the "shadcn" preset on https://ant.design/
export const shadcnThemeToken = {
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
colorPrimary: "#262626",
colorSuccess: "#22c55e",
colorWarning: "#f97316",
colorError: "#ef4444",
colorInfo: "#262626",
colorTextBase: "#262626",
colorBgBase: "#ffffff",
colorPrimaryBg: "#f5f5f5",
colorPrimaryBgHover: "#e5e5e5",
colorPrimaryBorder: "#d4d4d4",
colorPrimaryBorderHover: "#a3a3a3",
colorPrimaryHover: "#404040",
colorPrimaryActive: "#171717",
colorPrimaryText: "#262626",
colorPrimaryTextHover: "#404040",
colorPrimaryTextActive: "#171717",
colorSuccessBg: "#f0fdf4",
colorSuccessBgHover: "#dcfce7",
colorSuccessBorder: "#bbf7d0",
colorSuccessBorderHover: "#86efac",
colorSuccessHover: "#16a34a",
colorSuccessActive: "#15803d",
colorSuccessText: "#16a34a",
colorSuccessTextHover: "#16a34a",
colorSuccessTextActive: "#15803d",
colorWarningBg: "#fff7ed",
colorWarningBgHover: "#fed7aa",
colorWarningBorder: "#fdba74",
colorWarningBorderHover: "#fb923c",
colorWarningHover: "#ea580c",
colorWarningActive: "#c2410c",
colorWarningText: "#ea580c",
colorWarningTextHover: "#ea580c",
colorWarningTextActive: "#c2410c",
colorErrorBg: "#fef2f2",
colorErrorBgHover: "#fecaca",
colorErrorBorder: "#fca5a5",
colorErrorBorderHover: "#f87171",
colorErrorHover: "#dc2626",
colorErrorActive: "#b91c1c",
colorErrorText: "#dc2626",
colorErrorTextHover: "#dc2626",
colorErrorTextActive: "#b91c1c",
colorInfoBg: "#f5f5f5",
colorInfoBgHover: "#e5e5e5",
colorInfoBorder: "#d4d4d4",
colorInfoBorderHover: "#a3a3a3",
colorInfoHover: "#404040",
colorInfoActive: "#171717",
colorInfoText: "#262626",
colorInfoTextHover: "#404040",
colorInfoTextActive: "#171717",
colorText: "#262626",
colorTextSecondary: "#525252",
colorTextTertiary: "#737373",
colorTextQuaternary: "#a3a3a3",
colorTextDisabled: "#a3a3a3",
colorBgContainer: "#ffffff",
colorBgElevated: "#ffffff",
colorBgLayout: "#fafafa",
colorBgSpotlight: "rgba(38, 38, 38, 0.85)",
colorBgMask: "rgba(38, 38, 38, 0.45)",
colorBorder: "#e5e5e5",
colorBorderSecondary: "#f5f5f5",
borderRadius: 10,
borderRadiusXS: 2,
borderRadiusSM: 6,
borderRadiusLG: 14,
padding: 16,
paddingSM: 12,
paddingLG: 24,
margin: 16,
marginSM: 12,
marginLG: 24,
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
boxShadowSecondary: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
};
export const shadcnThemeComponents = {
Button: {
primaryShadow: "none",
defaultShadow: "none",
dangerShadow: "none",
defaultBorderColor: "#e4e4e7",
defaultColor: "#18181b",
defaultBg: "#ffffff",
defaultHoverBg: "#f4f4f5",
defaultHoverBorderColor: "#d4d4d8",
defaultHoverColor: "#18181b",
defaultActiveBg: "#e4e4e7",
defaultActiveBorderColor: "#d4d4d8",
borderRadius: 6,
},
Input: {
activeShadow: "none",
hoverBorderColor: "#a1a1aa",
activeBorderColor: "#18181b",
borderRadius: 6,
},
Select: {
optionSelectedBg: "#f4f4f5",
optionActiveBg: "#fafafa",
optionSelectedFontWeight: 500,
borderRadius: 6,
},
Alert: {
borderRadiusLG: 8,
},
Modal: {
borderRadiusLG: 12,
},
Progress: {
defaultColor: "#18181b",
remainingColor: "#f4f4f5",
},
Steps: {
iconSize: 32,
},
Switch: {
trackHeight: 24,
trackMinWidth: 44,
innerMinMargin: 4,
innerMaxMargin: 24,
},
Checkbox: {
borderRadiusSM: 4,
},
Slider: {
trackBg: "#f4f4f5",
trackHoverBg: "#e4e4e7",
handleSize: 18,
handleSizeHover: 20,
railSize: 6,
},
ColorPicker: {
borderRadius: 6,
},
Menu: {
itemFontSize: 14,
groupTitleFontSize: 12,
itemHeight: 40,
fontWeightStrong: 600,
},
Table: {
headerBg: "#fafafa",
headerSplitColor: "#e5e5e5",
fontWeightStrong: 600,
},
};

File diff suppressed because it is too large Load Diff