From 0c1c156bb79a6f9d45656b0bf1a4f0e0ba4768eb Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Tue, 8 Jul 2025 22:50:28 +0700 Subject: [PATCH] feat: Add Tabler UI support and enhance frontend with new features --- .gitea/workflows/docker-publish-tabler.yml | 37 + .gitea/workflows/docker-publish.yml | 8 +- DOCKER.md | 208 +++++ README.md | 72 +- frontend/README.md | 177 +++- frontend/index.html | 12 +- frontend/package-lock.json | 27 + frontend/package.json | 1 + frontend/src/App.css | 929 +++++++++++++++++++++ frontend/src/App.jsx | 173 +++- frontend/src/DataManager.jsx | 493 ++++++++--- 11 files changed, 1983 insertions(+), 154 deletions(-) create mode 100644 .gitea/workflows/docker-publish-tabler.yml create mode 100644 DOCKER.md diff --git a/.gitea/workflows/docker-publish-tabler.yml b/.gitea/workflows/docker-publish-tabler.yml new file mode 100644 index 0000000..3482dc8 --- /dev/null +++ b/.gitea/workflows/docker-publish-tabler.yml @@ -0,0 +1,37 @@ +name: Publish Tabler Docker image + +on: + push: + branches: + - tabler + +jobs: + build-and-push-tabler: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to Gitea Registry + uses: docker/login-action@v3 + with: + registry: git.shts.su + username: ${{ gitea.actor }} + password: ${{ secrets.ACTIONS_PAT }} + + - name: Build and push Tabler Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64 + tags: | + git.shts.su/${{ gitea.repository }}:tabler + git.shts.su/${{ gitea.repository }}:tabler-${{ gitea.sha }} + labels: | + org.opencontainers.image.title=S3 Lists Manager with Tabler UI + org.opencontainers.image.description=Modern S3 Lists Manager with Tabler UI design + org.opencontainers.image.version=tabler + org.opencontainers.image.revision=${{ gitea.sha }} + org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }} \ No newline at end of file diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml index 6cf1dfe..c9074e7 100644 --- a/.gitea/workflows/docker-publish.yml +++ b/.gitea/workflows/docker-publish.yml @@ -28,4 +28,10 @@ jobs: platforms: linux/amd64 tags: | git.shts.su/${{ gitea.repository }}:latest - git.shts.su/${{ gitea.repository }}:${{ gitea.sha }} \ No newline at end of file + git.shts.su/${{ gitea.repository }}:${{ gitea.sha }} + labels: | + org.opencontainers.image.title=S3 Lists Manager + org.opencontainers.image.description=S3 Lists Manager for domains and AS management + org.opencontainers.image.version=latest + org.opencontainers.image.revision=${{ gitea.sha }} + org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }} \ No newline at end of file diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..3b8bff9 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,208 @@ +# Docker Images + +Этот проект предоставляет Docker образы для S3 Lists Manager с двумя различными версиями интерфейса. + +## 🏷️ Доступные теги + +### Основная версия (main branch) +- `git.shts.su/[repository]:latest` - Стабильная версия с базовым интерфейсом +- `git.shts.su/[repository]:[commit-sha]` - Версия с конкретным коммитом + +### Tabler версия (tabler branch) +- `git.shts.su/[repository]:tabler` - Версия с современным Tabler UI +- `git.shts.su/[repository]:tabler-[commit-sha]` - Tabler версия с конкретным коммитом + +## 🚀 Запуск + +### Основная версия +```bash +docker run -d \ + --name s3-lists-manager \ + -p 3001:3001 \ + git.shts.su/[repository]:latest +``` + +### Tabler версия +```bash +docker run -d \ + --name s3-lists-manager-tabler \ + -p 3001:3001 \ + git.shts.su/[repository]:tabler +``` + +## 🔄 Обновление + +### Основная версия +```bash +docker pull git.shts.su/[repository]:latest +docker stop s3-lists-manager +docker rm s3-lists-manager +docker run -d \ + --name s3-lists-manager \ + -p 3001:3001 \ + git.shts.su/[repository]:latest +``` + +### Tabler версия +```bash +docker pull git.shts.su/[repository]:tabler +docker stop s3-lists-manager-tabler +docker rm s3-lists-manager-tabler +docker run -d \ + --name s3-lists-manager-tabler \ + -p 3001:3001 \ + git.shts.su/[repository]:tabler +``` + +## 🐳 Docker Compose + +Создайте файл `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + s3-lists-manager: + image: git.shts.su/[repository]:latest + container_name: s3-lists-manager + ports: + - "3001:3001" + restart: unless-stopped + environment: + - NODE_ENV=production + + s3-lists-manager-tabler: + image: git.shts.su/[repository]:tabler + container_name: s3-lists-manager-tabler + ports: + - "3002:3001" + restart: unless-stopped + environment: + - NODE_ENV=production +``` + +Запуск: +```bash +docker-compose up -d +``` + +## 🔧 Переменные окружения + +| Переменная | Описание | По умолчанию | +|------------|----------|--------------| +| `NODE_ENV` | Окружение Node.js | `production` | +| `PORT` | Порт для запуска сервера | `3001` | + +## 📊 Мониторинг + +### Просмотр логов +```bash +# Основная версия +docker logs s3-lists-manager + +# Tabler версия +docker logs s3-lists-manager-tabler +``` + +### Статистика контейнеров +```bash +docker stats s3-lists-manager s3-lists-manager-tabler +``` + +## 🔍 Отладка + +### Вход в контейнер +```bash +# Основная версия +docker exec -it s3-lists-manager /bin/bash + +# Tabler версия +docker exec -it s3-lists-manager-tabler /bin/bash +``` + +### Проверка процессов +```bash +docker exec s3-lists-manager ps aux +``` + +## 🏗️ Сборка локально + +### Основная версия +```bash +git checkout main +docker build -t s3-lists-manager:local . +``` + +### Tabler версия +```bash +git checkout tabler +docker build -t s3-lists-manager-tabler:local . +``` + +## 📝 CI/CD + +### Автоматическая сборка + +- **Ветка `main`**: Автоматически создает образ с тегом `latest` +- **Ветка `tabler`**: Автоматически создает образ с тегом `tabler` + +### Workflow файлы + +- `.gitea/workflows/docker-publish.yml` - Сборка основной версии +- `.gitea/workflows/docker-publish-tabler.yml` - Сборка Tabler версии + +## 🔒 Безопасность + +### Проверка образа +```bash +docker scan git.shts.su/[repository]:latest +docker scan git.shts.su/[repository]:tabler +``` + +### Запуск в изолированной сети +```bash +docker network create s3-lists-network +docker run -d \ + --name s3-lists-manager \ + --network s3-lists-network \ + -p 3001:3001 \ + git.shts.su/[repository]:latest +``` + +## 📈 Производительность + +### Ограничение ресурсов +```bash +docker run -d \ + --name s3-lists-manager \ + --memory=512m \ + --cpus=0.5 \ + -p 3001:3001 \ + git.shts.su/[repository]:latest +``` + +### Мониторинг ресурсов +```bash +docker stats --no-stream s3-lists-manager +``` + +## 🆘 Устранение неполадок + +### Проверка статуса контейнера +```bash +docker ps -a | grep s3-lists-manager +``` + +### Перезапуск контейнера +```bash +docker restart s3-lists-manager +``` + +### Очистка неиспользуемых образов +```bash +docker image prune -f +``` + +--- + +**Примечание**: Замените `[repository]` на актуальное имя вашего репозитория в Gitea. \ No newline at end of file diff --git a/README.md b/README.md index 7971461..8eb3510 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - **Сохранение в S3:** Все изменения сохраняются непосредственно в файлах в бакете Yandex Cloud. - **Docker-контейнеризация:** Готовый `Dockerfile` для сборки и запуска приложения в изолированном окружении. - **CI/CD с Gitea Actions:** Автоматическая сборка и публикация Docker-образа в Gitea Registry при пуше в `main`. +- **Два варианта интерфейса:** Базовая версия и современная версия с Tabler UI. ## 🛠️ Технологический стек @@ -19,6 +20,7 @@ |--------------|-----------------------------------------------------------------------------------------------------------| | **Фронтенд** | [**React**](https://reactjs.org/) + [**Vite**](https://vitejs.dev/) | | | [**React-Bootstrap**](https://react-bootstrap.github.io/) (UI-компоненты) | +| | [**Tabler Icons**](https://tabler-icons.io/) (иконки для Tabler версии) | | | [**Axios**](https://axios-http.com/) (HTTP-клиент) | | **Бэкенд** | [**Node.js**](https://nodejs.org/) + [**Express**](https://expressjs.com/) | | | [**AWS SDK for JS**](https://aws.amazon.com/sdk-for-javascript/) (для работы с Yandex Cloud S3) | @@ -103,9 +105,41 @@ graph TD ## 🐳 Docker -Приложение полностью готово к запуску в Docker. +Приложение полностью готово к запуску в Docker с двумя вариантами интерфейса. -### Сборка образа +### Доступные образы + +#### Основная версия (main branch) +```bash +git.shts.su/[repository]:latest +``` + +#### Tabler версия (tabler branch) +```bash +git.shts.su/[repository]:tabler +``` + +### Быстрый запуск + +#### Основная версия +```bash +docker run -d \ + --name s3-lists-manager \ + -p 3001:3001 \ + --env-file ./backend/.env \ + git.shts.su/[repository]:latest +``` + +#### Tabler версия +```bash +docker run -d \ + --name s3-lists-manager-tabler \ + -p 3002:3001 \ + --env-file ./backend/.env \ + git.shts.su/[repository]:tabler +``` + +### Локальная сборка Для сборки образа выполните команду в корневой директории проекта: ```bash @@ -122,6 +156,40 @@ docker run --rm -p 3001:3001 --env-file ./backend/.env s3-lists-manager После этого приложение будет доступно по адресу `http://localhost:3001`. +### Docker Compose + +Создайте файл `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + s3-lists-manager: + image: git.shts.su/[repository]:latest + container_name: s3-lists-manager + ports: + - "3001:3001" + restart: unless-stopped + env_file: + - ./backend/.env + + s3-lists-manager-tabler: + image: git.shts.su/[repository]:tabler + container_name: s3-lists-manager-tabler + ports: + - "3002:3001" + restart: unless-stopped + env_file: + - ./backend/.env +``` + +Запуск: +```bash +docker-compose up -d +``` + +Подробная документация по Docker образам доступна в файле [DOCKER.md](DOCKER.md). + ## ⚙️ API Endpoints | Метод | Путь | Описание | diff --git a/frontend/README.md b/frontend/README.md index 7059a96..1fd780d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,12 +1,175 @@ -# React + Vite +# S3 Lists Manager - Frontend -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Современный веб-интерфейс для управления списками доменов и AS с использованием Tabler UI. -Currently, two official plugins are available: +## 🎨 Особенности -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- **Современный UI/UX** - Полностью переработанный интерфейс в стиле Tabler +- **Адаптивный дизайн** - Отлично работает на всех устройствах +- **Иконки Tabler** - Красивые иконки для улучшения пользовательского опыта +- **Интерактивные элементы** - Анимации, hover-эффекты, модальные окна +- **Статистика** - Отображение количества элементов в реальном времени +- **Поиск** - Быстрый поиск по спискам +- **Массовые операции** - Возможность массового обновления шлюзов +- **Подтверждения** - Модальные окна для подтверждения важных действий -## Expanding the ESLint configuration +## 🚀 Технологии -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +- **React 19** - Современная библиотека для создания пользовательских интерфейсов +- **Tabler Icons** - Красивые иконки в стиле Tabler +- **CSS Variables** - Современные CSS переменные для легкой кастомизации +- **Responsive Design** - Адаптивный дизайн для всех устройств +- **Vite** - Быстрый сборщик проекта + +## 📦 Установка + +1. Убедитесь, что у вас установлен Node.js (версия 16 или выше) + +2. Перейдите в папку frontend: +```bash +cd frontend +``` + +3. Установите зависимости: +```bash +npm install +``` + +4. Запустите проект в режиме разработки: +```bash +npm run dev +``` + +5. Откройте браузер и перейдите по адресу: `http://localhost:5173` + +## 🏗️ Сборка для продакшена + +```bash +npm run build +``` + +Собранные файлы будут находиться в папке `dist/`. + +## 🎯 Использование + +### Управление доменами + +1. **Добавление домена**: + - Введите имя домена (например: `example.com`) + - Выберите шлюз (по умолчанию: `SharkFIN`) + - Нажмите "Добавить" + +2. **Редактирование**: + - Нажмите на иконку редактирования рядом с доменом + - Измените шлюз + - Нажмите "Сохранить" или "Отмена" + +3. **Удаление**: + - Нажмите на иконку удаления + - Подтвердите удаление в модальном окне + +### Управление AS + +Аналогично управлению доменами, но для AS номеров (например: `AS12345`). + +### Массовые операции + +1. Введите текущий шлюз в поле "Заменить с" +2. Введите новый шлюз в поле "Заменить на" +3. Нажмите "Выполнить замену" +4. Подтвердите операцию + +### Сохранение изменений + +После внесения изменений обязательно нажмите "Сохранить в S3" для записи изменений в базу данных. + +## 🎨 Кастомизация + +### Цветовая схема + +Основные цвета можно изменить в CSS переменных в файле `src/App.css`: + +```css +:root { + --tblr-primary: #206bc4; /* Основной цвет */ + --tblr-success: #2fb344; /* Цвет успеха */ + --tblr-warning: #f59f00; /* Цвет предупреждения */ + --tblr-danger: #d63939; /* Цвет ошибки */ +} +``` + +### Шрифты + +Проект использует шрифт Inter. Для изменения шрифта обновите переменную: + +```css +--tblr-font-family-sans-serif: "Ваш шрифт", -apple-system, BlinkMacSystemFont, ...; +``` + +## 📱 Адаптивность + +Интерфейс полностью адаптивен и оптимизирован для: + +- **Desktop** (1200px+) - Полный интерфейс с боковой панелью +- **Tablet** (768px - 1199px) - Адаптированный интерфейс +- **Mobile** (до 767px) - Мобильная версия с выпадающим меню + +## 🔧 Разработка + +### Структура проекта + +``` +src/ +├── App.jsx # Главный компонент приложения +├── App.css # Стили Tabler +├── DataManager.jsx # Компонент управления данными +├── main.jsx # Точка входа +└── index.css # Базовые стили +``` + +### Добавление новых функций + +1. Создайте новый компонент в папке `src/` +2. Импортируйте необходимые иконки из `@tabler/icons-react` +3. Используйте CSS классы Tabler для стилизации +4. Добавьте адаптивность для мобильных устройств + +### Иконки + +Все иконки импортируются из `@tabler/icons-react`: + +```jsx +import { IconPlus, IconEdit, IconTrash } from '@tabler/icons-react'; + +// Использование + +``` + +## 🐛 Отладка + +### Консоль разработчика + +Откройте консоль разработчика (F12) для просмотра ошибок и логов. + +### Проверка API + +Убедитесь, что бэкенд запущен и доступен по адресу `/api`. + +## 📄 Лицензия + +Этот проект использует лицензию MIT. + +## 🤝 Вклад в проект + +1. Форкните репозиторий +2. Создайте ветку для новой функции +3. Внесите изменения +4. Создайте Pull Request + +## 📞 Поддержка + +Если у вас есть вопросы или проблемы, создайте Issue в репозитории. + +--- + +**S3 Lists Manager** - Современный интерфейс для управления списками с использованием Tabler UI. diff --git a/frontend/index.html b/frontend/index.html index 0c589ec..08537b6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,18 @@ - + - Vite + React + + + + + + + + + S3 Lists Manager
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 534c99f..25a1e88 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tabler/icons-react": "^3.34.0", "axios": "^1.10.0", "bootstrap": "^5.3.7", "react": "^19.1.0", @@ -641,6 +642,32 @@ "tslib": "^2.8.0" } }, + "node_modules/@tabler/icons": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.0.tgz", + "integrity": "sha512-jtVqv0JC1WU2TTEBN32D9+R6mc1iEBuPwLnBsWaR02SIEciu9aq5806AWkCHuObhQ4ERhhXErLEK7Fs+tEZxiA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.34.0.tgz", + "integrity": "sha512-OpEIR2iZsIXECtAIMbn1zfKfQ3zKJjXyIZlkgOGUL9UkMCFycEiF2Y8AVfEQsyre/3FnBdlWJvGr0NU47n2TbQ==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.34.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index 1a67cbf..8093a2c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@tabler/icons-react": "^3.34.0", "axios": "^1.10.0", "bootstrap": "^5.3.7", "react": "^19.1.0", diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..2e8e033 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -40,3 +40,932 @@ .read-the-docs { color: #888; } + +/* Tabler CSS Variables */ +:root { + --tblr-primary: #206bc4; + --tblr-primary-rgb: 32, 107, 196; + --tblr-secondary: #6c757d; + --tblr-secondary-rgb: 108, 117, 125; + --tblr-success: #2fb344; + --tblr-success-rgb: 47, 179, 68; + --tblr-info: #467fd0; + --tblr-info-rgb: 70, 127, 208; + --tblr-warning: #f59f00; + --tblr-warning-rgb: 245, 159, 0; + --tblr-danger: #d63939; + --tblr-danger-rgb: 214, 57, 57; + --tblr-light: #f8f9fa; + --tblr-light-rgb: 248, 249, 250; + --tblr-dark: #1e293b; + --tblr-dark-rgb: 30, 41, 59; + --tblr-body-bg: #f5f6f7; + --tblr-body-color: #1e293b; + --tblr-border-color: #e9ecef; + --tblr-border-radius: 0.375rem; + --tblr-border-radius-sm: 0.25rem; + --tblr-border-radius-lg: 0.5rem; + --tblr-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --tblr-box-shadow-sm: 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.075); + --tblr-box-shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --tblr-font-family-sans-serif: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --tblr-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --tblr-font-size-base: 0.875rem; + --tblr-line-height-base: 1.5; + --tblr-heading-font-weight: 600; + --tblr-btn-font-weight: 500; + --tblr-btn-border-radius: 0.375rem; + --tblr-btn-padding-x: 0.75rem; + --tblr-btn-padding-y: 0.375rem; + --tblr-input-border-radius: 0.375rem; + --tblr-input-padding-x: 0.75rem; + --tblr-input-padding-y: 0.375rem; + --tblr-card-border-radius: 0.5rem; + --tblr-card-border-width: 0; + --tblr-card-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --tblr-navbar-height: 3.5rem; + --tblr-sidebar-width: 16rem; + --tblr-sidebar-width-md: 12rem; + --tblr-sidebar-width-sm: 4rem; +} + +/* Base Styles */ +* { + box-sizing: border-box; +} + +body { + font-family: var(--tblr-font-family-sans-serif); + font-size: var(--tblr-font-size-base); + line-height: var(--tblr-line-height-base); + color: var(--tblr-body-color); + background-color: var(--tblr-body-bg); + margin: 0; + padding: 0; +} + +/* Page Layout */ +.page { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.page-wrapper { + flex: 1; + display: flex; + flex-direction: column; +} + +.page-body { + flex: 1; + padding: 1.5rem 0; +} + +.page-header { + padding: 1.5rem 0; + border-bottom: 1px solid var(--tblr-border-color); + margin-bottom: 1.5rem; +} + +.page-title { + font-size: 1.5rem; + font-weight: var(--tblr-heading-font-weight); + margin: 0; + color: var(--tblr-dark); +} + +/* Navbar */ +.navbar { + background-color: #fff; + border-bottom: 1px solid var(--tblr-border-color); + box-shadow: var(--tblr-box-shadow-sm); + height: var(--tblr-navbar-height); + padding: 0; +} + +.navbar-brand { + font-weight: var(--tblr-heading-font-weight); + font-size: 1.25rem; + color: var(--tblr-dark); + text-decoration: none; + display: flex; + align-items: center; +} + +.navbar-brand:hover { + color: var(--tblr-primary); +} + +.navbar-brand-icon { + width: 1.5rem; + height: 1.5rem; +} + +.navbar-toggler { + border: none; + background: none; + padding: 0.5rem; + color: var(--tblr-secondary); + cursor: pointer; +} + +.navbar-toggler:hover { + color: var(--tblr-dark); +} + +.navbar-nav { + list-style: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; +} + +.nav-item { + margin: 0; +} + +.nav-link { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + color: var(--tblr-secondary); + text-decoration: none; + border-radius: var(--tblr-border-radius); + transition: all 0.15s ease-in-out; +} + +.nav-link:hover { + color: var(--tblr-dark); + background-color: var(--tblr-light); +} + +.nav-link.active { + color: var(--tblr-primary); + background-color: rgba(var(--tblr-primary-rgb), 0.1); +} + +.nav-link-icon { + width: 1.25rem; + height: 1.25rem; + margin-right: 0.5rem; +} + +.nav-link-title { + font-weight: 500; +} + +/* Avatar */ +.avatar { + display: inline-block; + width: 2rem; + height: 2rem; + border-radius: 50%; + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +.avatar-sm { + width: 1.5rem; + height: 1.5rem; +} + +/* Cards */ +.card { + background-color: #fff; + border: var(--tblr-card-border-width) solid var(--tblr-border-color); + border-radius: var(--tblr-card-border-radius); + box-shadow: var(--tblr-card-box-shadow); + margin-bottom: 1.5rem; + transition: box-shadow 0.15s ease-in-out; +} + +.card:hover { + box-shadow: var(--tblr-box-shadow-lg); +} + +.card-md { + max-width: 28rem; +} + +.card-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--tblr-border-color); + background-color: transparent; +} + +.card-title { + font-size: 1.125rem; + font-weight: var(--tblr-heading-font-weight); + margin: 0; + color: var(--tblr-dark); + display: flex; + align-items: center; +} + +.card-body { + padding: 1.5rem; +} + +.card-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Stats Cards */ +.h3 { + font-size: 1.75rem; + font-weight: var(--tblr-heading-font-weight); + margin: 0; + color: var(--tblr-dark); +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--tblr-btn-padding-y) var(--tblr-btn-padding-x); + font-size: var(--tblr-font-size-base); + font-weight: var(--tblr-btn-font-weight); + line-height: 1.5; + border: 1px solid transparent; + border-radius: var(--tblr-btn-border-radius); + cursor: pointer; + transition: all 0.15s ease-in-out; + text-decoration: none; + white-space: nowrap; +} + +.btn:hover { + text-decoration: none; + transform: translateY(-1px); + box-shadow: var(--tblr-box-shadow); +} + +.btn:disabled { + opacity: 0.65; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.btn-primary { + background-color: var(--tblr-primary); + border-color: var(--tblr-primary); + color: #fff; +} + +.btn-primary:hover { + background-color: #1a5aa3; + border-color: #1a5aa3; + color: #fff; +} + +.btn-secondary { + background-color: var(--tblr-secondary); + border-color: var(--tblr-secondary); + color: #fff; +} + +.btn-secondary:hover { + background-color: #5a6268; + border-color: #5a6268; + color: #fff; +} + +.btn-success { + background-color: var(--tblr-success); + border-color: var(--tblr-success); + color: #fff; +} + +.btn-success:hover { + background-color: #28a745; + border-color: #28a745; + color: #fff; +} + +.btn-warning { + background-color: var(--tblr-warning); + border-color: var(--tblr-warning); + color: #fff; +} + +.btn-warning:hover { + background-color: #e0a800; + border-color: #e0a800; + color: #fff; +} + +.btn-danger { + background-color: var(--tblr-danger); + border-color: var(--tblr-danger); + color: #fff; +} + +.btn-danger:hover { + background-color: #c82333; + border-color: #c82333; + color: #fff; +} + +.btn-outline-primary { + background-color: transparent; + border-color: var(--tblr-primary); + color: var(--tblr-primary); +} + +.btn-outline-primary:hover { + background-color: var(--tblr-primary); + border-color: var(--tblr-primary); + color: #fff; +} + +.btn-outline-secondary { + background-color: transparent; + border-color: var(--tblr-secondary); + color: var(--tblr-secondary); +} + +.btn-outline-secondary:hover { + background-color: var(--tblr-secondary); + border-color: var(--tblr-secondary); + color: #fff; +} + +.btn-outline-danger { + background-color: transparent; + border-color: var(--tblr-danger); + color: var(--tblr-danger); +} + +.btn-outline-danger:hover { + background-color: var(--tblr-danger); + border-color: var(--tblr-danger); + color: #fff; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.btn-list { + display: flex; + gap: 0.25rem; +} + +/* Forms */ +.form-label { + font-weight: 500; + color: var(--tblr-dark); + margin-bottom: 0.5rem; +} + +.form-control { + display: block; + width: 100%; + padding: var(--tblr-input-padding-y) var(--tblr-input-padding-x); + font-size: var(--tblr-font-size-base); + line-height: var(--tblr-line-height-base); + color: var(--tblr-body-color); + background-color: #fff; + border: 1px solid var(--tblr-border-color); + border-radius: var(--tblr-input-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-control:focus { + border-color: var(--tblr-primary); + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(var(--tblr-primary-rgb), 0.25); +} + +.form-control-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.form-footer { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--tblr-border-color); +} + +/* Input Icon */ +.input-icon { + position: relative; +} + +.input-icon .icon { + position: absolute; + left: 0.75rem; + top: 50%; + transform: translateY(-50%); + color: var(--tblr-secondary); + z-index: 1; +} + +.input-icon .form-control { + padding-left: 2.5rem; +} + +/* Tables */ +.table { + width: 100%; + margin-bottom: 1rem; + color: var(--tblr-body-color); + border-collapse: collapse; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid var(--tblr-border-color); +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid var(--tblr-border-color); + font-weight: 600; + color: var(--tblr-dark); + background-color: var(--tblr-light); +} + +.table tbody tr:hover { + background-color: rgba(var(--tblr-primary-rgb), 0.05); +} + +.table-vcenter td { + vertical-align: middle; +} + +.card-table { + margin-bottom: 0; +} + +.card-table th:first-child, +.card-table td:first-child { + padding-left: 1.5rem; +} + +.card-table th:last-child, +.card-table td:last-child { + padding-right: 1.5rem; +} + +/* Badges */ +.badge { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: var(--tblr-border-radius-sm); +} + +.bg-blue-lt { + background-color: rgba(var(--tblr-primary-rgb), 0.1); + color: var(--tblr-primary); +} + +/* Alerts */ +.alert { + position: relative; + padding: 1rem 1.5rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: var(--tblr-border-radius); +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.alert-dismissible { + padding-right: 3rem; +} + +.btn-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 1rem 1.5rem; + background: transparent; + border: 0; + cursor: pointer; +} + +.btn-close::before { + content: "×"; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + opacity: 0.5; +} + +.btn-close:hover::before { + opacity: 0.75; +} + +.alert-icon { + width: 1.25rem; + height: 1.25rem; + margin-right: 0.5rem; +} + +/* Empty States */ +.empty { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + margin-bottom: 1rem; +} + +.empty-icon .icon { + width: 3rem; + height: 3rem; + color: var(--tblr-secondary); +} + +.empty-title { + font-size: 1.25rem; + font-weight: var(--tblr-heading-font-weight); + color: var(--tblr-dark); + margin: 0 0 0.5rem 0; +} + +.empty-subtitle { + font-size: var(--tblr-font-size-base); + margin: 0; +} + +/* Icons */ +.icon { + width: 1rem; + height: 1rem; + display: inline-block; + vertical-align: middle; +} + +/* Modal */ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1050; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(2px); +} + +.modal-dialog { + max-width: 500px; + width: 90%; + margin: 1.75rem auto; + animation: modalFadeIn 0.3s ease-out; +} + +.modal-content { + background-color: #fff; + border-radius: var(--tblr-card-border-radius); + box-shadow: var(--tblr-box-shadow-lg); + border: none; +} + +.modal-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--tblr-border-color); + display: flex; + align-items: center; +} + +.modal-title { + font-size: 1.125rem; + font-weight: var(--tblr-heading-font-weight); + margin: 0; + color: var(--tblr-dark); +} + +.modal-body { + padding: 1.5rem; +} + +.modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--tblr-border-color); + display: flex; + gap: 0.5rem; + justify-content: flex-end; +} + +@keyframes modalFadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Utilities */ +.text-muted { + color: var(--tblr-secondary) !important; +} + +.text-center { + text-align: center !important; +} + +.font-weight-medium { + font-weight: 500 !important; +} + +.w-1 { + width: 1rem !important; +} + +.me-2 { + margin-right: 0.5rem !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +/* Footer */ +.footer { + background-color: transparent; + border-top: 1px solid var(--tblr-border-color); + padding: 1rem 0; + margin-top: auto; +} + +.footer-transparent { + background-color: transparent; +} + +.list-inline { + list-style: none; + margin: 0; + padding: 0; +} + +.list-inline-item { + display: inline-block; +} + +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.list-inline-dots .list-inline-item:not(:last-child)::after { + content: "·"; + margin-left: 0.5rem; + color: var(--tblr-secondary); +} + +.link-secondary { + color: var(--tblr-secondary); + text-decoration: none; +} + +.link-secondary:hover { + color: var(--tblr-dark); + text-decoration: underline; +} + +/* Responsive */ +@media (max-width: 768px) { + .navbar-expand-md .navbar-collapse { + display: none; + } + + .navbar-expand-md.show .navbar-collapse { + display: block; + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: #fff; + border-top: 1px solid var(--tblr-border-color); + box-shadow: var(--tblr-box-shadow); + z-index: 1000; + } + + .navbar-expand-md .navbar-nav { + flex-direction: column; + } + + .navbar-expand-md .nav-item { + width: 100%; + } + + .navbar-expand-md .nav-link { + padding: 1rem; + border-radius: 0; + } + + .card-actions { + flex-direction: column; + gap: 0.5rem; + } + + .btn-list { + flex-direction: column; + } + + .modal-footer { + flex-direction: column; + } + + .modal-footer .btn { + width: 100%; + } +} + +/* Container */ +.container-xl { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Row and Col */ +.row { + display: flex; + flex-wrap: wrap; + margin: 0 -0.75rem; +} + +.col, +.col-lg-4, +.col-lg-8 { + padding: 0 0.75rem; +} + +.col { + flex: 1; +} + +.col-lg-4 { + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.col-lg-8 { + flex: 0 0 66.666667%; + max-width: 66.666667%; +} + +@media (max-width: 992px) { + .col-lg-4, + .col-lg-8 { + flex: 0 0 100%; + max-width: 100%; + } +} + +/* Spinner */ +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spinner-border 0.75s linear infinite; +} + +@keyframes spinner-border { + to { + transform: rotate(360deg); + } +} + +.text-primary { + color: var(--tblr-primary) !important; +} + +.visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +/* Enhanced hover effects */ +.card:hover .card-title { + color: var(--tblr-primary); +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: var(--tblr-box-shadow); +} + +.form-control:focus { + transform: translateY(-1px); + box-shadow: 0 0 0 0.2rem rgba(var(--tblr-primary-rgb), 0.25), var(--tblr-box-shadow); +} + +/* Loading states */ +.btn:disabled { + transform: none !important; + box-shadow: none !important; +} + +/* Focus states for accessibility */ +.btn:focus, +.form-control:focus, +.nav-link:focus { + outline: 2px solid var(--tblr-primary); + outline-offset: 2px; +} + +/* Dark mode support (future enhancement) */ +@media (prefers-color-scheme: dark) { + :root { + --tblr-body-bg: #1a1a1a; + --tblr-body-color: #e9ecef; + --tblr-dark: #f8f9fa; + --tblr-border-color: #2d3748; + } + + .card { + background-color: #2d3748; + border-color: #4a5568; + } + + .navbar { + background-color: #2d3748; + border-color: #4a5568; + } + + .form-control { + background-color: #4a5568; + border-color: #718096; + color: #e9ecef; + } + + .table { + color: #e9ecef; + } + + .table thead th { + background-color: #4a5568; + color: #f8f9fa; + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 49d5c06..f8cd04f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,40 +1,149 @@ +import { useState } from 'react'; import { Container, Navbar, Tabs, Tab } from 'react-bootstrap'; +import { + IconBrandTabler, + IconWorld, + IconNetwork, + IconSettings, + IconMenu2, + IconX +} from '@tabler/icons-react'; import DataManager from './DataManager'; +import './App.css'; function App() { - return ( - <> - - - - - - - - S3 Lists Manager - - - + const [sidebarOpen, setSidebarOpen] = useState(false); + const [activeTab, setActiveTab] = useState('domains'); - - - - - - - - - - - + const toggleSidebar = () => setSidebarOpen(!sidebarOpen); + + return ( +
+ {/* Header */} +
+
+ +

+ + S3 Lists Manager +

+
+
+ + + +
+ Profile + Settings +
+ Logout +
+
+
+
+
+ + {/* Sidebar */} +
+ +
+ + {/* Main Content */} +
+
+
+
+
+

+ {activeTab === 'domains' ? 'Управление доменами' : 'Управление AS'} +

+
+ {activeTab === 'domains' + ? 'Добавляйте, редактируйте и управляйте списками доменов' + : 'Добавляйте, редактируйте и управляйте списками AS' + } +
+
+
+
+
+ +
+
+ {activeTab === 'domains' && ( + + )} + {activeTab === 'asns' && ( + + )} +
+
+ + {/* Footer */} +
+
+
+
+ +
+
+
+
+
+
); } diff --git a/frontend/src/DataManager.jsx b/frontend/src/DataManager.jsx index b92e333..82b10b1 100644 --- a/frontend/src/DataManager.jsx +++ b/frontend/src/DataManager.jsx @@ -1,6 +1,20 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; import { Table, Button, Form, Row, Col, Card, Alert } from 'react-bootstrap'; +import { + IconPlus, + IconSearch, + IconEdit, + IconTrash, + IconCheck, + IconX, + IconDatabase, + IconRefresh, + IconArrowsExchange, + IconDownload, + IconUpload, + IconAlertTriangle +} from '@tabler/icons-react'; const API_URL = '/api'; @@ -14,18 +28,25 @@ function DataManager({ entityName, entityKey, placeholder }) { const [editingValue, setEditingValue] = useState(''); const [bulkFrom, setBulkFrom] = useState('SharkFIN'); const [bulkTo, setBulkTo] = useState('SharkAM'); + const [loading, setLoading] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [itemToDelete, setItemToDelete] = useState(null); useEffect(() => { fetchItems(); }, []); const fetchItems = async () => { + setLoading(true); try { const response = await axios.get(`${API_URL}/${entityKey}`); setItems(response.data); + setError(''); } catch (error) { console.error(`Error fetching ${entityKey}:`, error); setError(`Не удалось загрузить ${entityName}. Проверьте, запущен ли бэкенд.`); + } finally { + setLoading(false); } }; @@ -60,15 +81,30 @@ function DataManager({ entityName, entityKey, placeholder }) { setItems(items.filter(i => i.domain !== domainNameToDelete)); }; + const confirmDelete = (item) => { + setItemToDelete(item); + setShowDeleteModal(true); + }; + + const executeDelete = () => { + if (itemToDelete) { + handleDeleteItem(itemToDelete.domain); + setShowDeleteModal(false); + setItemToDelete(null); + } + }; + const handleSaveChanges = async () => { + setLoading(true); try { - // The backend expects a payload with a 'domains' key for both endpoints. await axios.post(`${API_URL}/${entityKey}`, { domains: items }); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); } catch (error) { console.error('Error saving changes:', error); setError('Не удалось сохранить изменения.'); + } finally { + setLoading(false); } }; @@ -93,130 +129,367 @@ function DataManager({ entityName, entityKey, placeholder }) { return ( <> - {error && setError('')} dismissible>{error}} - {success && setSuccess('')} dismissible>{success}} + {error && ( +
+
+
+ +
+
+ {error} +
+
+ setError('')}> +
+ )} + + {success && ( +
+
+
+ +
+
+ {success} +
+
+ setSuccess('')}> +
+ )} - - - - Добавить новый {entityName} - -
{ e.preventDefault(); handleAddItem(); }}> - - Имя {entityName} - +
+ {/* Add New Item Card */} +
+
+

+ + Добавить новый {entityName} +

+
+
+ { e.preventDefault(); handleAddItem(); }}> +
+ + setNewItem({ ...newItem, domain: e.target.value })} /> - - - Шлюз - +
+ + setNewItem({ ...newItem, type: e.target.value })} /> - - - - - +
+
+ +
+ +
+
- - Массовое обновление - - - Заменить с - setBulkFrom(e.target.value)} /> - - - Заменить на - setBulkTo(e.target.value)} /> - - - - - - - - - -
-
Список: {entityName}
- + {/* Bulk Update Card */} +
+
+

+ + Массовое обновление +

+
+
+
+ + setBulkFrom(e.target.value)} + />
- - - setSearchTerm(e.target.value)} - /> - - - - - - - - - - - {filteredItems.map((item, index) => ( - - - - - +
+ + setBulkTo(e.target.value)} + /> +
+
+ +
+ + + + {/* Stats Card */} +
+
+

+ + Статистика +

+
+
+
+
+
Всего
+
{items.length}
+
+
+
Найдено
+
{filteredItems.length}
+
+
+
+
+ + +
+ {/* Main Data Card */} +
+
+
+

+ + Список: {entityName} +

+
+ + +
+
+
+
+ {/* Search */} +
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ + {/* Table */} +
+
#{entityName}ШлюзДействия
{index + 1}{item.domain} - {editingDomain === item.domain ? ( - setEditingValue(e.target.value)} - autoFocus - /> - ) : ( - item.type - )} - - {editingDomain === item.domain ? ( - <> - - - - ) : ( - <> - - - - )} -
+ + + + + + - ))} - -
#{entityName}Шлюз
+ + + {filteredItems.map((item, index) => ( + + {index + 1} + +
{item.domain}
+ + + {editingDomain === item.domain ? ( + setEditingValue(e.target.value)} + autoFocus + /> + ) : ( + {item.type} + )} + + + {editingDomain === item.domain ? ( +
+ + +
+ ) : ( +
+ + +
+ )} + + + ))} + + +
+ + {/* Empty States */} {items.length > 0 && filteredItems.length === 0 && ( -

По вашему запросу ничего не найдено.

+
+
+ +
+

Ничего не найдено

+

+ По вашему запросу ничего не найдено. +

+
)} - {items.length === 0 &&

Список пуст. Добавьте новый элемент.

} - - - - + + {items.length === 0 && !loading && ( +
+
+ +
+

Список пуст

+

+ Добавьте новый элемент, чтобы начать работу. +

+
+ )} + + {loading && ( +
+
+ Загрузка... +
+
+ )} +
+
+
+
+ + {/* Delete Confirmation Modal */} + {showDeleteModal && ( +
+
+
+
+ +
Подтверждение удаления
+ +
+
+

Вы уверены, что хотите удалить {itemToDelete?.domain}?

+

Это действие нельзя отменить.

+
+
+ + +
+
+
+
+ )} ); }