Remove execution_mode dependencies from InstalledScriptsTab (#50)
- Remove ExecutionModeBadge import and usage - Update filtering logic to use server_name presence instead of execution_mode - Simplify update script logic by removing mode property - Update Terminal component call to determine mode based on server presence - Replace ExecutionModeBadge in table with simple text display - Maintain backend API compatibility by keeping execution_mode in mutations - Use nullish coalescing operator (??) for better null handling
This commit is contained in:
committed by
GitHub
parent
e09c1bbf5d
commit
b366a33f07
68
package-lock.json
generated
68
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "pve-scripts-local",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.0.0",
|
||||
@@ -19,6 +20,8 @@
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"better-sqlite3": "^9.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"next": "^15.5.3",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
@@ -28,6 +31,7 @@
|
||||
"server-only": "^0.0.1",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"superjson": "^2.2.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -1927,6 +1931,39 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.34",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.34.tgz",
|
||||
@@ -4353,12 +4390,33 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
@@ -9606,6 +9664,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
|
||||
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.0.0",
|
||||
@@ -33,6 +34,8 @@
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"better-sqlite3": "^9.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"next": "^15.5.3",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
@@ -42,6 +45,7 @@
|
||||
"server-only": "^0.0.1",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"superjson": "^2.2.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
|
||||
82
scripts/ct/2fauth.sh
Normal file
82
scripts/ct/2fauth.sh
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
source "$SCRIPT_DIR/../core/build.func"
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Author: jkrgr0
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://docs.2fauth.app/
|
||||
|
||||
APP="2FAuth"
|
||||
var_tags="${var_tags:-2fa;authenticator}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d "/opt/2fauth" ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
if check_for_gh_release "2fauth" "Bubka/2FAuth"; then
|
||||
$STD apt update
|
||||
$STD apt -y upgrade
|
||||
|
||||
msg_info "Creating Backup"
|
||||
mv "/opt/2fauth" "/opt/2fauth-backup"
|
||||
if ! dpkg -l | grep -q 'php8.3'; then
|
||||
cp /etc/nginx/conf.d/2fauth.conf /etc/nginx/conf.d/2fauth.conf.bak
|
||||
fi
|
||||
msg_ok "Backup Created"
|
||||
|
||||
if ! dpkg -l | grep -q 'php8.3'; then
|
||||
$STD apt-get install -y \
|
||||
lsb-release \
|
||||
gnupg2
|
||||
PHP_VERSION="8.3" PHP_MODULE="common,ctype,fileinfo,mysql,cli" PHP_FPM="YES" setup_php
|
||||
sed -i 's/php8.2/php8.3/g' /etc/nginx/conf.d/2fauth.conf
|
||||
fi
|
||||
fetch_and_deploy_gh_release "2fauth" "Bubka/2FAuth"
|
||||
setup_composer
|
||||
mv "/opt/2fauth-backup/.env" "/opt/2fauth/.env"
|
||||
mv "/opt/2fauth-backup/storage" "/opt/2fauth/storage"
|
||||
cd "/opt/2fauth" || return
|
||||
chown -R www-data: "/opt/2fauth"
|
||||
chmod -R 755 "/opt/2fauth"
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
$STD composer install --no-dev --prefer-source
|
||||
php artisan 2fauth:install
|
||||
$STD systemctl restart nginx
|
||||
|
||||
msg_info "Cleaning Up"
|
||||
if dpkg -l | grep -q 'php8.2'; then
|
||||
$STD apt remove --purge -y php8.2*
|
||||
fi
|
||||
$STD apt -y autoremove
|
||||
$STD apt -y autoclean
|
||||
$STD apt -y clean
|
||||
msg_ok "Cleanup Completed"
|
||||
msg_ok "Updated Successfully"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:80${CL}"
|
||||
105
scripts/install/2fauth-install.sh
Normal file
105
scripts/install/2fauth-install.sh
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Author: jkrgr0
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://docs.2fauth.app/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
lsb-release \
|
||||
nginx
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PHP_VERSION="8.3" PHP_MODULE="common,ctype,fileinfo,mysql,cli" PHP_FPM="YES" setup_php
|
||||
setup_composer
|
||||
setup_mariadb
|
||||
|
||||
msg_info "Setting up Database"
|
||||
DB_NAME=2fauth_db
|
||||
DB_USER=2fauth
|
||||
DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
|
||||
$STD mariadb -u root -e "CREATE DATABASE $DB_NAME;"
|
||||
$STD mariadb -u root -e "CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';"
|
||||
$STD mariadb -u root -e "GRANT ALL ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;"
|
||||
{
|
||||
echo "2FAuth Credentials"
|
||||
echo "Database User: $DB_USER"
|
||||
echo "Database Password: $DB_PASS"
|
||||
echo "Database Name: $DB_NAME"
|
||||
} >>~/2FAuth.creds
|
||||
msg_ok "Set up Database"
|
||||
|
||||
fetch_and_deploy_gh_release "2fauth" "Bubka/2FAuth"
|
||||
|
||||
msg_info "Setup 2FAuth"
|
||||
cd /opt/2fauth || exit
|
||||
cp .env.example .env
|
||||
IPADDRESS=$(hostname -I | awk '{print $1}')
|
||||
sed -i -e "s|^APP_URL=.*|APP_URL=http://$IPADDRESS|" \
|
||||
-e "s|^DB_CONNECTION=$|DB_CONNECTION=mysql|" \
|
||||
-e "s|^DB_DATABASE=$|DB_DATABASE=$DB_NAME|" \
|
||||
-e "s|^DB_HOST=$|DB_HOST=127.0.0.1|" \
|
||||
-e "s|^DB_PORT=$|DB_PORT=3306|" \
|
||||
-e "s|^DB_USERNAME=$|DB_USERNAME=$DB_USER|" \
|
||||
-e "s|^DB_PASSWORD=$|DB_PASSWORD=$DB_PASS|" .env
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
$STD composer update --no-plugins --no-scripts
|
||||
$STD composer install --no-dev --prefer-source --no-plugins --no-scripts
|
||||
$STD php artisan key:generate --force
|
||||
$STD php artisan migrate:refresh
|
||||
$STD php artisan passport:install -q -n
|
||||
$STD php artisan storage:link
|
||||
$STD php artisan config:cache
|
||||
chown -R www-data: /opt/2fauth
|
||||
chmod -R 755 /opt/2fauth
|
||||
msg_ok "Setup 2fauth"
|
||||
|
||||
msg_info "Configure Service"
|
||||
cat <<EOF >/etc/nginx/conf.d/2fauth.conf
|
||||
server {
|
||||
listen 80;
|
||||
root /opt/2fauth/public;
|
||||
server_name $IPADDRESS;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
error_page 404 /index.php;
|
||||
|
||||
location ~ \.php\$ {
|
||||
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
systemctl reload nginx
|
||||
msg_ok "Configured Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
|
||||
msg_info "Cleaning up"
|
||||
$STD apt -y autoremove
|
||||
$STD apt -y autoclean
|
||||
$STD apt -y clean
|
||||
msg_ok "Cleaned"
|
||||
@@ -1,4 +1,84 @@
|
||||
[
|
||||
{
|
||||
"name": "sassanix/Warracker",
|
||||
"version": "0.10.1.14",
|
||||
"date": "2025-10-06T23:35:16Z"
|
||||
},
|
||||
{
|
||||
"name": "outline/outline",
|
||||
"version": "v1.0.0-1",
|
||||
"date": "2025-10-06T23:16:32Z"
|
||||
},
|
||||
{
|
||||
"name": "Ombi-app/Ombi",
|
||||
"version": "v4.47.1",
|
||||
"date": "2025-01-05T21:14:23Z"
|
||||
},
|
||||
{
|
||||
"name": "Kometa-Team/Kometa",
|
||||
"version": "v2.2.2",
|
||||
"date": "2025-10-06T21:31:07Z"
|
||||
},
|
||||
{
|
||||
"name": "booklore-app/booklore",
|
||||
"version": "v1.5.0",
|
||||
"date": "2025-10-06T20:56:57Z"
|
||||
},
|
||||
{
|
||||
"name": "grokability/snipe-it",
|
||||
"version": "v8.3.3",
|
||||
"date": "2025-10-06T19:57:17Z"
|
||||
},
|
||||
{
|
||||
"name": "meilisearch/meilisearch",
|
||||
"version": "prototype-shorten-snapshot-creation-2",
|
||||
"date": "2025-10-06T19:36:54Z"
|
||||
},
|
||||
{
|
||||
"name": "TwiN/gatus",
|
||||
"version": "v5.26.0",
|
||||
"date": "2025-10-06T17:57:27Z"
|
||||
},
|
||||
{
|
||||
"name": "seerr-team/seerr",
|
||||
"version": "preview-seerr",
|
||||
"date": "2025-10-06T16:50:29Z"
|
||||
},
|
||||
{
|
||||
"name": "zwave-js/zwave-js-ui",
|
||||
"version": "v11.4.0",
|
||||
"date": "2025-10-06T16:08:51Z"
|
||||
},
|
||||
{
|
||||
"name": "fuma-nama/fumadocs",
|
||||
"version": "fumadocs-ui@15.8.4",
|
||||
"date": "2025-10-06T15:41:49Z"
|
||||
},
|
||||
{
|
||||
"name": "bunkerity/bunkerweb",
|
||||
"version": "v1.6.5",
|
||||
"date": "2025-10-06T15:25:17Z"
|
||||
},
|
||||
{
|
||||
"name": "bastienwirtz/homer",
|
||||
"version": "v25.10.1",
|
||||
"date": "2025-10-06T14:23:20Z"
|
||||
},
|
||||
{
|
||||
"name": "chrisvel/tududi",
|
||||
"version": "v0.83",
|
||||
"date": "2025-10-06T13:49:52Z"
|
||||
},
|
||||
{
|
||||
"name": "dgtlmoon/changedetection.io",
|
||||
"version": "0.50.16",
|
||||
"date": "2025-10-06T13:40:13Z"
|
||||
},
|
||||
{
|
||||
"name": "n8n-io/n8n",
|
||||
"version": "n8n@1.114.3",
|
||||
"date": "2025-10-06T12:22:22Z"
|
||||
},
|
||||
{
|
||||
"name": "Graylog2/graylog2-server",
|
||||
"version": "7.0.0-beta.3",
|
||||
@@ -29,11 +109,6 @@
|
||||
"version": "v0.24.82",
|
||||
"date": "2025-10-06T07:56:13Z"
|
||||
},
|
||||
{
|
||||
"name": "dgtlmoon/changedetection.io",
|
||||
"version": "0.50.15",
|
||||
"date": "2025-10-06T07:15:01Z"
|
||||
},
|
||||
{
|
||||
"name": "firefly-iii/firefly-iii",
|
||||
"version": "v6.4.0",
|
||||
@@ -74,11 +149,6 @@
|
||||
"version": "4.5.3",
|
||||
"date": "2025-08-25T13:59:56Z"
|
||||
},
|
||||
{
|
||||
"name": "outline/outline",
|
||||
"version": "v1.0.0-0",
|
||||
"date": "2025-10-05T20:30:31Z"
|
||||
},
|
||||
{
|
||||
"name": "plankanban/planka",
|
||||
"version": "planka-1.0.5",
|
||||
@@ -101,19 +171,14 @@
|
||||
},
|
||||
{
|
||||
"name": "runtipi/runtipi",
|
||||
"version": "v4.4.0",
|
||||
"date": "2025-09-02T19:26:18Z"
|
||||
"version": "nightly",
|
||||
"date": "2025-10-05T14:13:25Z"
|
||||
},
|
||||
{
|
||||
"name": "Prowlarr/Prowlarr",
|
||||
"version": "v2.0.5.5160",
|
||||
"date": "2025-08-23T21:23:11Z"
|
||||
},
|
||||
{
|
||||
"name": "chrisvel/tududi",
|
||||
"version": "v0.82-rc5",
|
||||
"date": "2025-09-23T07:31:12Z"
|
||||
},
|
||||
{
|
||||
"name": "TandoorRecipes/recipes",
|
||||
"version": "2.3.0",
|
||||
@@ -159,11 +224,6 @@
|
||||
"version": "2.520",
|
||||
"date": "2025-10-05T00:51:34Z"
|
||||
},
|
||||
{
|
||||
"name": "Ombi-app/Ombi",
|
||||
"version": "v4.47.1",
|
||||
"date": "2025-01-05T21:14:23Z"
|
||||
},
|
||||
{
|
||||
"name": "ollama/ollama",
|
||||
"version": "v0.12.4-rc5",
|
||||
@@ -224,16 +284,6 @@
|
||||
"version": "2025.10.1",
|
||||
"date": "2025-10-03T18:10:59Z"
|
||||
},
|
||||
{
|
||||
"name": "fuma-nama/fumadocs",
|
||||
"version": "@fumadocs/mdx-remote@1.4.2",
|
||||
"date": "2025-10-03T17:01:32Z"
|
||||
},
|
||||
{
|
||||
"name": "bunkerity/bunkerweb",
|
||||
"version": "v1.6.5",
|
||||
"date": "2025-10-03T16:43:34Z"
|
||||
},
|
||||
{
|
||||
"name": "immich-app/immich",
|
||||
"version": "v2.0.1",
|
||||
@@ -259,11 +309,6 @@
|
||||
"version": "v0.30.1",
|
||||
"date": "2025-10-03T06:55:25Z"
|
||||
},
|
||||
{
|
||||
"name": "booklore-app/booklore",
|
||||
"version": "v1.4.1",
|
||||
"date": "2025-10-03T06:52:35Z"
|
||||
},
|
||||
{
|
||||
"name": "redis/redis",
|
||||
"version": "8.2.2",
|
||||
@@ -279,16 +324,6 @@
|
||||
"version": "v0.9.95",
|
||||
"date": "2025-10-02T16:07:18Z"
|
||||
},
|
||||
{
|
||||
"name": "meilisearch/meilisearch",
|
||||
"version": "prototype-shorten-snapshot-creation-0",
|
||||
"date": "2025-10-02T15:16:05Z"
|
||||
},
|
||||
{
|
||||
"name": "n8n-io/n8n",
|
||||
"version": "n8n@1.112.6",
|
||||
"date": "2025-09-26T10:56:27Z"
|
||||
},
|
||||
{
|
||||
"name": "theonedev/onedev",
|
||||
"version": "v13.0.7",
|
||||
@@ -389,11 +424,6 @@
|
||||
"version": "v4.4.2",
|
||||
"date": "2025-09-30T20:16:13Z"
|
||||
},
|
||||
{
|
||||
"name": "TwiN/gatus",
|
||||
"version": "v5.25.2",
|
||||
"date": "2025-09-30T18:32:35Z"
|
||||
},
|
||||
{
|
||||
"name": "WordPress/WordPress",
|
||||
"version": "4.7.31",
|
||||
@@ -414,11 +444,6 @@
|
||||
"version": "4.4.46",
|
||||
"date": "2025-09-30T13:21:24Z"
|
||||
},
|
||||
{
|
||||
"name": "fallenbagel/jellyseerr",
|
||||
"version": "preview-rename-tags",
|
||||
"date": "2025-09-30T12:50:15Z"
|
||||
},
|
||||
{
|
||||
"name": "emqx/emqx",
|
||||
"version": "e6.0.0",
|
||||
@@ -459,11 +484,6 @@
|
||||
"version": "v2.7.12",
|
||||
"date": "2025-05-29T17:08:26Z"
|
||||
},
|
||||
{
|
||||
"name": "sassanix/Warracker",
|
||||
"version": "0.10.1.13",
|
||||
"date": "2025-09-29T17:11:25Z"
|
||||
},
|
||||
{
|
||||
"name": "AdguardTeam/AdGuardHome",
|
||||
"version": "v0.107.67",
|
||||
@@ -536,8 +556,8 @@
|
||||
},
|
||||
{
|
||||
"name": "javedh-dev/tracktor",
|
||||
"version": "0.3.17",
|
||||
"date": "2025-09-27T07:00:36Z"
|
||||
"version": "0.3.18",
|
||||
"date": "2025-09-27T10:32:09Z"
|
||||
},
|
||||
{
|
||||
"name": "Dolibarr/dolibarr",
|
||||
@@ -554,11 +574,6 @@
|
||||
"version": "v4.104.2",
|
||||
"date": "2025-09-26T22:34:32Z"
|
||||
},
|
||||
{
|
||||
"name": "bastienwirtz/homer",
|
||||
"version": "v25.09.1",
|
||||
"date": "2025-09-26T19:22:16Z"
|
||||
},
|
||||
{
|
||||
"name": "traefik/traefik",
|
||||
"version": "v3.5.3",
|
||||
@@ -624,11 +639,6 @@
|
||||
"version": "v1.9.10",
|
||||
"date": "2025-09-24T13:49:53Z"
|
||||
},
|
||||
{
|
||||
"name": "zwave-js/zwave-js-ui",
|
||||
"version": "v11.3.1",
|
||||
"date": "2025-09-24T11:58:00Z"
|
||||
},
|
||||
{
|
||||
"name": "syncthing/syncthing",
|
||||
"version": "v2.0.10",
|
||||
@@ -719,11 +729,6 @@
|
||||
"version": "v0.23.2",
|
||||
"date": "2025-09-18T17:18:59Z"
|
||||
},
|
||||
{
|
||||
"name": "grokability/snipe-it",
|
||||
"version": "v8.3.2",
|
||||
"date": "2025-09-18T13:55:58Z"
|
||||
},
|
||||
{
|
||||
"name": "NLnetLabs/unbound",
|
||||
"version": "release-1.24.0",
|
||||
@@ -1039,11 +1044,6 @@
|
||||
"version": "latest",
|
||||
"date": "2025-08-15T15:33:51Z"
|
||||
},
|
||||
{
|
||||
"name": "Kometa-Team/Kometa",
|
||||
"version": "v2.2.1",
|
||||
"date": "2025-08-13T19:49:01Z"
|
||||
},
|
||||
{
|
||||
"name": "swapplications/uhf-server-dist",
|
||||
"version": "1.5.1",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"documentation": "https://www.zigbee2mqtt.io/guide/getting-started/",
|
||||
"website": "https://www.zigbee2mqtt.io/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/zigbee2mqtt.webp",
|
||||
"config_path": "/opt/zigbee2mqtt/data/configuration.yaml",
|
||||
"config_path": "debian: /opt/zigbee2mqtt/data/configuration.yaml | alpine: /var/lib/zigbee2mqtt/configuration.yaml",
|
||||
"description": "Zigbee2MQTT is an open-source software project that allows you to use Zigbee-based smart home devices (such as those sold under the Philips Hue and Ikea Tradfri brands) with MQTT-based home automation systems, like Home Assistant, Node-RED, and others. The software acts as a bridge between your Zigbee devices and MQTT, allowing you to control and monitor these devices from your home automation system.",
|
||||
"install_methods": [
|
||||
{
|
||||
|
||||
@@ -16,15 +16,15 @@ export function Badge({ variant, type, noteType, status, executionMode, children
|
||||
const getTypeStyles = (scriptType: string) => {
|
||||
switch (scriptType.toLowerCase()) {
|
||||
case 'ct':
|
||||
return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-700';
|
||||
return 'bg-primary/10 text-primary border-primary/20';
|
||||
case 'addon':
|
||||
return 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200 border-purple-200 dark:border-purple-700';
|
||||
return 'bg-purple-500/10 text-purple-400 border-purple-500/20';
|
||||
case 'vm':
|
||||
return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border-green-200 dark:border-green-700';
|
||||
return 'bg-green-500/10 text-green-400 border-green-500/20';
|
||||
case 'pve':
|
||||
return 'bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-200 border-orange-200 dark:border-orange-700';
|
||||
return 'bg-orange-500/10 text-orange-400 border-orange-500/20';
|
||||
default:
|
||||
return 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 border-gray-200 dark:border-gray-600';
|
||||
return 'bg-muted text-muted-foreground border-border';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,45 +34,45 @@ export function Badge({ variant, type, noteType, status, executionMode, children
|
||||
return `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${type ? getTypeStyles(type) : getTypeStyles('unknown')}`;
|
||||
|
||||
case 'updateable':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20';
|
||||
|
||||
case 'privileged':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20';
|
||||
|
||||
case 'status':
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20';
|
||||
case 'failed':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20';
|
||||
case 'in_progress':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-500/10 text-yellow-400 border border-yellow-500/20';
|
||||
default:
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 border border-gray-200 dark:border-gray-600';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground border border-border';
|
||||
}
|
||||
|
||||
case 'execution-mode':
|
||||
switch (executionMode) {
|
||||
case 'local':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border border-blue-200 dark:border-blue-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/10 text-primary border border-primary/20';
|
||||
case 'ssh':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200 border border-purple-200 dark:border-purple-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20';
|
||||
default:
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 border border-gray-200 dark:border-gray-600';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground border border-border';
|
||||
}
|
||||
|
||||
case 'note':
|
||||
switch (noteType) {
|
||||
case 'warning':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-500/10 text-yellow-400 border border-yellow-500/20';
|
||||
case 'error':
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20';
|
||||
default:
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border border-blue-200 dark:border-blue-700';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/10 text-primary border border-primary/20';
|
||||
}
|
||||
|
||||
default:
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 border border-gray-200 dark:border-gray-600';
|
||||
return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground border border-border';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -195,24 +195,24 @@ export function CategorySidebar({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 transition-all duration-300 ${
|
||||
<div className={`bg-card rounded-lg shadow-md border border-border transition-all duration-300 ${
|
||||
isCollapsed ? 'w-16' : 'w-80'
|
||||
}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
{!isCollapsed && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Categories</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{totalScripts} Total scripts</p>
|
||||
<h3 className="text-lg font-semibold text-foreground">Categories</h3>
|
||||
<p className="text-sm text-muted-foreground">{totalScripts} Total scripts</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
title={isCollapsed ? 'Expand categories' : 'Collapse categories'}
|
||||
>
|
||||
<svg
|
||||
className={`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform ${
|
||||
className={`w-5 h-5 text-muted-foreground transition-transform ${
|
||||
isCollapsed ? 'rotate-180' : ''
|
||||
}`}
|
||||
fill="none"
|
||||
@@ -231,23 +231,23 @@ export function CategorySidebar({
|
||||
{/* "All Categories" option */}
|
||||
<button
|
||||
onClick={() => onCategorySelect(null)}
|
||||
className={`w-full flex items-center justify-between p-3 rounded-lg text-left transition-colors ${
|
||||
selectedCategory === null
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
className={`w-full flex items-center justify-between p-3 rounded-lg text-left transition-colors ${
|
||||
selectedCategory === null
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'hover:bg-accent text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<CategoryIcon
|
||||
iconName="template"
|
||||
className={`w-5 h-5 ${selectedCategory === null ? 'text-blue-500' : 'text-gray-500 dark:text-gray-400'}`}
|
||||
className={`w-5 h-5 ${selectedCategory === null ? 'text-primary' : 'text-muted-foreground'}`}
|
||||
/>
|
||||
<span className="font-medium">All Categories</span>
|
||||
</div>
|
||||
<span className={`text-sm px-2 py-1 rounded-full ${
|
||||
selectedCategory === null
|
||||
? 'bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-300'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}>
|
||||
{totalScripts}
|
||||
</span>
|
||||
@@ -263,14 +263,14 @@ export function CategorySidebar({
|
||||
onClick={() => onCategorySelect(category)}
|
||||
className={`w-full flex items-center justify-between p-3 rounded-lg text-left transition-colors ${
|
||||
isSelected
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'hover:bg-accent text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<CategoryIcon
|
||||
iconName={categoryIconMapping[category] ?? 'box'}
|
||||
className={`w-5 h-5 ${isSelected ? 'text-blue-500' : 'text-gray-500 dark:text-gray-400'}`}
|
||||
className={`w-5 h-5 ${isSelected ? 'text-primary' : 'text-muted-foreground'}`}
|
||||
/>
|
||||
<span className="font-medium capitalize">
|
||||
{category.replace(/[_-]/g, ' ')}
|
||||
@@ -278,8 +278,8 @@ export function CategorySidebar({
|
||||
</div>
|
||||
<span className={`text-sm px-2 py-1 rounded-full ${
|
||||
isSelected
|
||||
? 'bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-300'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}>
|
||||
{count}
|
||||
</span>
|
||||
@@ -297,27 +297,27 @@ export function CategorySidebar({
|
||||
<div className="group relative">
|
||||
<button
|
||||
onClick={() => onCategorySelect(null)}
|
||||
className={`w-12 h-12 rounded-lg flex flex-col items-center justify-center transition-colors relative ${
|
||||
selectedCategory === null
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
className={`w-12 h-12 rounded-lg flex flex-col items-center justify-center transition-colors relative ${
|
||||
selectedCategory === null
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'hover:bg-accent text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<CategoryIcon
|
||||
iconName="template"
|
||||
className={`w-5 h-5 ${selectedCategory === null ? 'text-blue-500' : 'text-gray-500 dark:text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-200'}`}
|
||||
className={`w-5 h-5 ${selectedCategory === null ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'}`}
|
||||
/>
|
||||
<span className={`text-xs mt-1 px-1 rounded ${
|
||||
selectedCategory === null
|
||||
? 'bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-300'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}>
|
||||
{totalScripts}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute left-full ml-2 top-1/2 transform -translate-y-1/2 bg-gray-900 dark:bg-gray-700 text-white text-sm px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
|
||||
<div className="absolute left-full ml-2 top-1/2 transform -translate-y-1/2 bg-gray-900 text-white text-sm px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
|
||||
All Categories ({totalScripts})
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,25 +332,25 @@ export function CategorySidebar({
|
||||
onClick={() => onCategorySelect(category)}
|
||||
className={`w-12 h-12 rounded-lg flex flex-col items-center justify-center transition-colors relative ${
|
||||
isSelected
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'hover:bg-accent text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<CategoryIcon
|
||||
iconName={categoryIconMapping[category] ?? 'box'}
|
||||
className={`w-5 h-5 ${isSelected ? 'text-blue-500' : 'text-gray-500 dark:text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-200'}`}
|
||||
className={`w-5 h-5 ${isSelected ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'}`}
|
||||
/>
|
||||
<span className={`text-xs mt-1 px-1 rounded ${
|
||||
isSelected
|
||||
? 'bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-300'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute left-full ml-2 top-1/2 transform -translate-y-1/2 bg-gray-900 dark:bg-gray-700 text-white text-sm px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
|
||||
<div className="absolute left-full ml-2 top-1/2 transform -translate-y-1/2 bg-gray-900 text-white text-sm px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
|
||||
{category} ({count})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
interface DarkModeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
const DarkModeContext = createContext<DarkModeContextType | undefined>(undefined);
|
||||
|
||||
export function DarkModeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>('system');
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Initialize theme from localStorage after mount
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('theme') as Theme;
|
||||
if (stored && ['light', 'dark', 'system'].includes(stored)) {
|
||||
setThemeState(stored);
|
||||
}
|
||||
|
||||
// Set initial isDark state based on current DOM state
|
||||
const currentlyDark = document.documentElement.classList.contains('dark');
|
||||
setIsDark(currentlyDark);
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Update dark mode state and DOM when theme changes
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
const updateDarkMode = () => {
|
||||
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const shouldBeDark = theme === 'dark' || (theme === 'system' && systemDark);
|
||||
|
||||
// Only update if there's actually a change
|
||||
if (shouldBeDark !== isDark) {
|
||||
setIsDark(shouldBeDark);
|
||||
|
||||
// Apply to document
|
||||
if (shouldBeDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateDarkMode();
|
||||
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = () => {
|
||||
if (theme === 'system') {
|
||||
updateDarkMode();
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, [theme, mounted, isDark]);
|
||||
|
||||
const setTheme = (newTheme: Theme) => {
|
||||
setThemeState(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<DarkModeContext.Provider value={{ theme, setTheme, isDark }}>
|
||||
{children}
|
||||
</DarkModeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDarkMode() {
|
||||
const context = useContext(DarkModeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useDarkMode must be used within a DarkModeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useDarkMode } from './DarkModeProvider';
|
||||
|
||||
export function DarkModeToggle() {
|
||||
const { theme, setTheme, isDark } = useDarkMode();
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (theme === 'light') {
|
||||
setTheme('dark');
|
||||
} else if (theme === 'dark') {
|
||||
setTheme('system');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
if (theme === 'light') {
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
} else if (theme === 'dark') {
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
// System theme icon
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7l.159-1.591A3.001 3.001 0 0112 8a3 3 0 01-3.229 2.409L8.771 12z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getLabel = () => {
|
||||
if (theme === 'light') return 'Light mode';
|
||||
if (theme === 'dark') return 'Dark mode';
|
||||
return 'System theme';
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className={`
|
||||
flex items-center justify-center
|
||||
w-10 h-10 rounded-lg
|
||||
transition-all duration-200
|
||||
hover:scale-105 active:scale-95
|
||||
${isDark
|
||||
? 'bg-gray-800 text-yellow-400 hover:bg-gray-700 border border-gray-600'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-300'
|
||||
}
|
||||
`}
|
||||
title={getLabel()}
|
||||
aria-label={getLabel()}
|
||||
>
|
||||
{getIcon()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -45,17 +45,17 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer
|
||||
key={index}
|
||||
className={`flex font-mono text-sm ${
|
||||
isAdded
|
||||
? 'bg-green-50 text-green-800 border-l-4 border-green-400'
|
||||
? 'bg-green-500/10 text-green-400 border-l-4 border-green-500'
|
||||
: isRemoved
|
||||
? 'bg-red-50 text-red-800 border-l-4 border-red-400'
|
||||
: 'bg-gray-50 text-gray-700'
|
||||
? 'bg-destructive/10 text-destructive border-l-4 border-destructive'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<div className="w-16 text-right pr-2 text-gray-500 select-none">
|
||||
<div className="w-16 text-right pr-2 text-muted-foreground select-none">
|
||||
{lineNumber}
|
||||
</div>
|
||||
<div className="flex-1 pl-2">
|
||||
<span className={isAdded ? 'text-green-600' : isRemoved ? 'text-red-600' : ''}>
|
||||
<span className={isAdded ? 'text-green-400' : isRemoved ? 'text-destructive' : ''}>
|
||||
{isAdded ? '+' : isRemoved ? '-' : ' '}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap">{content}</span>
|
||||
@@ -66,27 +66,27 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4 z-50"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">Script Diff</h2>
|
||||
<p className="text-sm text-gray-600">{filePath}</p>
|
||||
<h2 className="text-xl font-bold text-foreground">Script Diff</h2>
|
||||
<p className="text-sm text-muted-foreground">{filePath}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors disabled:opacity-50"
|
||||
className="px-3 py-1 text-sm bg-primary/10 text-primary rounded hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -96,19 +96,19 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="px-4 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<div className="px-4 py-2 bg-muted border-b border-border">
|
||||
<div className="flex items-center space-x-4 text-sm">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-3 h-3 bg-green-100 border border-green-300"></div>
|
||||
<span className="text-green-700">Added (Remote)</span>
|
||||
<div className="w-3 h-3 bg-green-500/20 border border-green-500/40"></div>
|
||||
<span className="text-green-400">Added (Remote)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-3 h-3 bg-red-100 border border-red-300"></div>
|
||||
<span className="text-red-700">Removed (Local)</span>
|
||||
<div className="w-3 h-3 bg-destructive/20 border border-destructive/40"></div>
|
||||
<span className="text-destructive">Removed (Local)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-3 h-3 bg-gray-100 border border-gray-300"></div>
|
||||
<span className="text-gray-700">Unchanged</span>
|
||||
<div className="w-3 h-3 bg-muted border border-border"></div>
|
||||
<span className="text-muted-foreground">Unchanged</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,14 +117,14 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer
|
||||
<div className="overflow-y-auto max-h-[calc(90vh-120px)]">
|
||||
{diffData?.success ? (
|
||||
diffData.diff ? (
|
||||
<div className="divide-y divide-gray-200">
|
||||
<div className="divide-y divide-border">
|
||||
{diffData.diff.split('\n').map((line, index) =>
|
||||
line.trim() ? renderDiffLine(line, index) : null
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<svg className="w-12 h-12 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="p-8 text-center text-muted-foreground">
|
||||
<svg className="w-12 h-12 mx-auto mb-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p>No differences found</p>
|
||||
@@ -132,16 +132,16 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer
|
||||
</div>
|
||||
)
|
||||
) : diffData?.error ? (
|
||||
<div className="p-8 text-center text-red-500">
|
||||
<svg className="w-12 h-12 mx-auto mb-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="p-8 text-center text-destructive">
|
||||
<svg className="w-12 h-12 mx-auto mb-4 text-destructive" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p>Error loading diff</p>
|
||||
<p className="text-sm">{diffData.error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<div className="p-8 text-center text-muted-foreground">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p>Loading diff...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Server } from '../../types/server';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface ExecutionModeModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -60,40 +61,42 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-md w-full mx-4 border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900">Execution Mode</h2>
|
||||
<button
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<h2 className="text-xl font-bold text-foreground">Execution Mode</h2>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
Where would you like to execute "{scriptName}"?
|
||||
</h3>
|
||||
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<div className="mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<svg className="h-5 w-5 text-destructive" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-red-700">{error}</p>
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,8 +110,8 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-colors ${
|
||||
selectedMode === 'ssh'
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
onClick={() => handleModeChange('ssh')}
|
||||
>
|
||||
@@ -120,20 +123,20 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
value="ssh"
|
||||
checked={selectedMode === 'ssh'}
|
||||
onChange={() => handleModeChange('ssh')}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
||||
className="h-4 w-4 text-primary focus:ring-primary border-border"
|
||||
/>
|
||||
<label htmlFor="ssh" className="ml-3 flex-1 cursor-pointer">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h4 className="text-sm font-medium text-gray-900">SSH Execution</h4>
|
||||
<p className="text-sm text-gray-500">Run the script on a remote server</p>
|
||||
<h4 className="text-sm font-medium text-foreground">SSH Execution</h4>
|
||||
<p className="text-sm text-muted-foreground">Run the script on a remote server</p>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
@@ -144,16 +147,16 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
{/* Server Selection (only for SSH mode) */}
|
||||
{selectedMode === 'ssh' && (
|
||||
<div className="mb-6">
|
||||
<label htmlFor="server" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label htmlFor="server" className="block text-sm font-medium text-foreground mb-2">
|
||||
Select Server
|
||||
</label>
|
||||
{loading ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-2 text-sm text-gray-600">Loading servers...</p>
|
||||
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Loading servers...</p>
|
||||
</div>
|
||||
) : servers.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500">
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
<p className="text-sm">No servers configured</p>
|
||||
<p className="text-xs mt-1">Add servers in Settings to use SSH execution</p>
|
||||
</div>
|
||||
@@ -166,7 +169,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
const server = servers.find(s => s.id === serverId);
|
||||
setSelectedServer(server ?? null);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
className="w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary bg-background text-foreground"
|
||||
>
|
||||
<option value="">Select a server...</option>
|
||||
{servers.map((server) => (
|
||||
@@ -181,23 +184,22 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExecute}
|
||||
disabled={selectedMode === 'ssh' && !selectedServer}
|
||||
className={`px-4 py-2 text-sm font-medium text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
|
||||
selectedMode === 'ssh' && !selectedServer
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700'
|
||||
}`}
|
||||
variant="default"
|
||||
size="default"
|
||||
className={selectedMode === 'ssh' && !selectedServer ? 'bg-gray-400 cursor-not-allowed' : ''}
|
||||
>
|
||||
{selectedMode === 'local' ? 'Run Locally' : 'Run on Server'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export interface FilterState {
|
||||
searchQuery: string;
|
||||
@@ -74,13 +75,13 @@ export function FilterBar({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="mb-6 rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
{/* Search Bar */}
|
||||
<div className="mb-4">
|
||||
<div className="relative max-w-md">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg
|
||||
className="h-5 w-5 text-gray-400 dark:text-gray-500"
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -98,12 +99,14 @@ export function FilterBar({
|
||||
placeholder="Search scripts..."
|
||||
value={filters.searchQuery}
|
||||
onChange={(e) => updateFilters({ searchQuery: e.target.value })}
|
||||
className="block w-full rounded-lg border border-gray-300 bg-white py-3 pr-10 pl-10 text-sm leading-5 text-gray-900 placeholder-gray-500 focus:border-blue-500 focus:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:border-blue-400 dark:focus:placeholder-gray-300 dark:focus:ring-blue-400"
|
||||
className="block w-full rounded-lg border border-input bg-background py-3 pr-10 pl-10 text-sm leading-5 text-foreground placeholder-muted-foreground focus:border-primary focus:placeholder-muted-foreground focus:ring-2 focus:ring-primary focus:outline-none"
|
||||
/>
|
||||
{filters.searchQuery && (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => updateFilters({ searchQuery: "" })}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute inset-y-0 right-0 pr-3 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
@@ -118,7 +121,7 @@ export function FilterBar({
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,7 +129,7 @@ export function FilterBar({
|
||||
{/* Filter Buttons */}
|
||||
<div className="mb-4 flex flex-wrap gap-3">
|
||||
{/* Updateable Filter */}
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
const next =
|
||||
filters.showUpdatable === null
|
||||
@@ -136,25 +139,29 @@ export function FilterBar({
|
||||
: null;
|
||||
updateFilters({ showUpdatable: next });
|
||||
}}
|
||||
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
||||
filters.showUpdatable === null
|
||||
? "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
: filters.showUpdatable === true
|
||||
? "border border-green-300 bg-green-100 text-green-800 dark:border-green-700 dark:bg-green-900/50 dark:text-green-200"
|
||||
: "border border-red-300 bg-red-100 text-red-800 dark:border-red-700 dark:bg-red-900/50 dark:text-red-200"
|
||||
}`}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className={`${
|
||||
filters.showUpdatable === null
|
||||
? "bg-muted text-muted-foreground hover:bg-accent"
|
||||
: filters.showUpdatable === true
|
||||
? "border border-green-500/20 bg-green-500/10 text-green-400"
|
||||
: "border border-destructive/20 bg-destructive/10 text-destructive"
|
||||
}`}
|
||||
>
|
||||
{getUpdatableButtonText()}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Type Dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setIsTypeDropdownOpen(!isTypeDropdownOpen)}
|
||||
className={`flex items-center space-x-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
||||
variant="outline"
|
||||
size="default"
|
||||
className={`flex items-center space-x-2 ${
|
||||
filters.selectedTypes.length === 0
|
||||
? "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
: "border border-cyan-300 bg-cyan-100 text-cyan-800 dark:border-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-200"
|
||||
? "bg-muted text-muted-foreground hover:bg-accent"
|
||||
: "border border-primary/20 bg-primary/10 text-primary"
|
||||
}`}
|
||||
>
|
||||
<span>{getTypeButtonText()}</span>
|
||||
@@ -171,15 +178,15 @@ export function FilterBar({
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{isTypeDropdownOpen && (
|
||||
<div className="absolute top-full left-0 z-10 mt-1 w-48 rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="absolute top-full left-0 z-10 mt-1 w-48 rounded-lg border border-border bg-card shadow-lg">
|
||||
<div className="p-2">
|
||||
{SCRIPT_TYPES.map((type) => (
|
||||
<label
|
||||
key={type.value}
|
||||
className="flex cursor-pointer items-center space-x-3 rounded-md px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="flex cursor-pointer items-center space-x-3 rounded-md px-3 py-2 hover:bg-accent"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -200,25 +207,27 @@ export function FilterBar({
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
|
||||
className="rounded border-input text-primary focus:ring-primary"
|
||||
/>
|
||||
<span className="text-lg">{type.icon}</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{type.label}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-gray-200 p-2 dark:border-gray-700">
|
||||
<button
|
||||
<div className="border-t border-border p-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateFilters({ selectedTypes: [] });
|
||||
setIsTypeDropdownOpen(false);
|
||||
}}
|
||||
className="w-full rounded-md px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -232,20 +241,22 @@ export function FilterBar({
|
||||
onChange={(e) =>
|
||||
updateFilters({ sortBy: e.target.value as "name" | "created" })
|
||||
}
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:focus:ring-blue-400"
|
||||
className="rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-primary focus:outline-none"
|
||||
>
|
||||
<option value="name">📝 By Name</option>
|
||||
<option value="created">📅 By Created Date</option>
|
||||
</select>
|
||||
|
||||
{/* Sort Order Button */}
|
||||
<button
|
||||
<Button
|
||||
onClick={() =>
|
||||
updateFilters({
|
||||
sortOrder: filters.sortOrder === "asc" ? "desc" : "asc",
|
||||
})
|
||||
}
|
||||
className="flex items-center space-x-1 rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex items-center space-x-1 bg-muted text-muted-foreground hover:bg-accent"
|
||||
>
|
||||
{filters.sortOrder === "asc" ? (
|
||||
<>
|
||||
@@ -286,20 +297,20 @@ export function FilterBar({
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Summary and Clear All */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{filteredCount === totalScripts ? (
|
||||
<span>Showing all {totalScripts} scripts</span>
|
||||
) : (
|
||||
<span>
|
||||
{filteredCount} of {totalScripts} scripts{" "}
|
||||
{hasActiveFilters && (
|
||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||
<span className="font-medium text-blue-600">
|
||||
(filtered)
|
||||
</span>
|
||||
)}
|
||||
@@ -308,9 +319,11 @@ export function FilterBar({
|
||||
</div>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
<Button
|
||||
onClick={clearAllFilters}
|
||||
className="flex items-center space-x-1 rounded-md px-3 py-1 text-sm text-red-600 transition-colors hover:bg-red-50 hover:text-red-800 dark:text-red-400 dark:hover:bg-red-900/20 dark:hover:text-red-300"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center space-x-1 text-red-600 hover:bg-red-50 hover:text-red-800"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
@@ -326,7 +339,7 @@ export function FilterBar({
|
||||
/>
|
||||
</svg>
|
||||
<span>Clear all filters</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { useState } from 'react';
|
||||
import { api } from '~/trpc/react';
|
||||
import { Terminal } from './Terminal';
|
||||
import { StatusBadge, ExecutionModeBadge } from './Badge';
|
||||
import { StatusBadge } from './Badge';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface InstalledScript {
|
||||
id: number;
|
||||
@@ -15,7 +16,6 @@ interface InstalledScript {
|
||||
server_ip: string | null;
|
||||
server_user: string | null;
|
||||
server_password: string | null;
|
||||
execution_mode: 'local' | 'ssh';
|
||||
installation_date: string;
|
||||
status: 'in_progress' | 'success' | 'failed';
|
||||
output_log: string | null;
|
||||
@@ -25,7 +25,7 @@ export function InstalledScriptsTab() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'success' | 'failed' | 'in_progress'>('all');
|
||||
const [serverFilter, setServerFilter] = useState<string>('all');
|
||||
const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any; mode: 'local' | 'ssh' } | null>(null);
|
||||
const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null);
|
||||
const [editingScriptId, setEditingScriptId] = useState<number | null>(null);
|
||||
const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' });
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
@@ -80,7 +80,7 @@ export function InstalledScriptsTab() {
|
||||
const matchesStatus = statusFilter === 'all' || script.status === statusFilter;
|
||||
|
||||
const matchesServer = serverFilter === 'all' ||
|
||||
(serverFilter === 'local' && script.execution_mode === 'local') ||
|
||||
(serverFilter === 'local' && !script.server_name) ||
|
||||
(script.server_name === serverFilter);
|
||||
|
||||
return matchesSearch && matchesStatus && matchesServer;
|
||||
@@ -111,7 +111,7 @@ export function InstalledScriptsTab() {
|
||||
if (confirm(`Are you sure you want to update ${script.script_name}?`)) {
|
||||
// Get server info if it's SSH mode
|
||||
let server = null;
|
||||
if (script.execution_mode === 'ssh' && script.server_id && script.server_user && script.server_password) {
|
||||
if (script.server_id && script.server_user && script.server_password) {
|
||||
server = {
|
||||
id: script.server_id,
|
||||
name: script.server_name,
|
||||
@@ -124,8 +124,7 @@ export function InstalledScriptsTab() {
|
||||
setUpdatingScript({
|
||||
id: script.id,
|
||||
containerId: script.container_id,
|
||||
server: server,
|
||||
mode: script.execution_mode
|
||||
server: server
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -205,7 +204,7 @@ export function InstalledScriptsTab() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500 dark:text-gray-400">Loading installed scripts...</div>
|
||||
<div className="text-muted-foreground">Loading installed scripts...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -218,7 +217,7 @@ export function InstalledScriptsTab() {
|
||||
<Terminal
|
||||
scriptPath={`update-${updatingScript.containerId}`}
|
||||
onClose={handleCloseUpdateTerminal}
|
||||
mode={updatingScript.mode}
|
||||
mode={updatingScript.server ? 'ssh' : 'local'}
|
||||
server={updatingScript.server}
|
||||
isUpdate={true}
|
||||
containerId={updatingScript.containerId}
|
||||
@@ -227,79 +226,80 @@ export function InstalledScriptsTab() {
|
||||
)}
|
||||
|
||||
{/* Header with Stats */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4">Installed Scripts</h2>
|
||||
<div className="bg-card rounded-lg shadow p-6">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-4">Installed Scripts</h2>
|
||||
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.total}</div>
|
||||
<div className="text-sm text-blue-800">Total Installations</div>
|
||||
<div className="bg-blue-500/10 border border-blue-500/20 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-400">{stats.total}</div>
|
||||
<div className="text-sm text-blue-300">Total Installations</div>
|
||||
</div>
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">{stats.byStatus.success}</div>
|
||||
<div className="text-sm text-green-800">Successful</div>
|
||||
<div className="bg-green-500/10 border border-green-500/20 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-400">{stats.byStatus.success}</div>
|
||||
<div className="text-sm text-green-300">Successful</div>
|
||||
</div>
|
||||
<div className="bg-red-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">{stats.byStatus.failed}</div>
|
||||
<div className="text-sm text-red-800">Failed</div>
|
||||
<div className="bg-red-500/10 border border-red-500/20 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-400">{stats.byStatus.failed}</div>
|
||||
<div className="text-sm text-red-300">Failed</div>
|
||||
</div>
|
||||
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-yellow-600">{stats.byStatus.in_progress}</div>
|
||||
<div className="text-sm text-yellow-800">In Progress</div>
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/20 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-yellow-400">{stats.byStatus.in_progress}</div>
|
||||
<div className="text-sm text-yellow-300">In Progress</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Script Button */}
|
||||
<div className="mb-4">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
variant={showAddForm ? "outline" : "default"}
|
||||
size="default"
|
||||
>
|
||||
{showAddForm ? 'Cancel Add Script' : '+ Add Manual Script Entry'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Add Script Form */}
|
||||
{showAddForm && (
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Add Manual Script Entry</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<div className="mb-6 p-6 bg-card rounded-lg border border-border shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-6">Add Manual Script Entry</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
Script Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={addFormData.script_name}
|
||||
onChange={(e) => handleAddFormChange('script_name', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
placeholder="Enter script name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
Container ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={addFormData.container_id}
|
||||
onChange={(e) => handleAddFormChange('container_id', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
placeholder="Enter container ID"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
Server
|
||||
</label>
|
||||
<select
|
||||
value={addFormData.server_id}
|
||||
onChange={(e) => handleAddFormChange('server_id', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
>
|
||||
<option value="local">Select Server (Local if none)</option>
|
||||
<option value="local">Select Server</option>
|
||||
{serversData?.servers?.map((server: any) => (
|
||||
<option key={server.id} value={server.id}>
|
||||
{server.name}
|
||||
@@ -308,20 +308,22 @@ export function InstalledScriptsTab() {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-3 mt-4">
|
||||
<button
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<Button
|
||||
onClick={handleCancelAdd}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddScript}
|
||||
disabled={createScriptMutation.isPending}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
variant="default"
|
||||
size="default"
|
||||
>
|
||||
{createScriptMutation.isPending ? 'Adding...' : 'Add Script'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -334,14 +336,14 @@ export function InstalledScriptsTab() {
|
||||
placeholder="Search scripts, container IDs, or servers..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as 'all' | 'success' | 'failed' | 'in_progress')}
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
className="px-3 py-2 border border-border rounded-md bg-card text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="success">Success</option>
|
||||
@@ -352,7 +354,7 @@ export function InstalledScriptsTab() {
|
||||
<select
|
||||
value={serverFilter}
|
||||
onChange={(e) => setServerFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
className="px-3 py-2 border border-border rounded-md bg-card text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="all">All Servers</option>
|
||||
<option value="local">Local</option>
|
||||
@@ -364,42 +366,39 @@ export function InstalledScriptsTab() {
|
||||
</div>
|
||||
|
||||
{/* Scripts Table */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||
<div className="bg-card rounded-lg shadow overflow-hidden">
|
||||
{filteredScripts.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{scripts.length === 0 ? 'No installed scripts found.' : 'No scripts match your filters.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-muted">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Script Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Container ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Server
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
Mode
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Installation Date
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody className="bg-card divide-y divide-gray-200">
|
||||
{filteredScripts.map((script) => (
|
||||
<tr key={script.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<tr key={script.id} className="hover:bg-accent">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{editingScriptId === script.id ? (
|
||||
<div className="space-y-2">
|
||||
@@ -407,15 +406,15 @@ export function InstalledScriptsTab() {
|
||||
type="text"
|
||||
value={editFormData.script_name}
|
||||
onChange={(e) => handleInputChange('script_name', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-2 py-1 text-sm border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Script name"
|
||||
/>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{script.script_path}</div>
|
||||
<div className="text-xs text-muted-foreground">{script.script_path}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{script.script_name}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">{script.script_path}</div>
|
||||
<div className="text-sm font-medium text-foreground">{script.script_name}</div>
|
||||
<div className="text-sm text-muted-foreground">{script.script_path}</div>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
@@ -425,81 +424,76 @@ export function InstalledScriptsTab() {
|
||||
type="text"
|
||||
value={editFormData.container_id}
|
||||
onChange={(e) => handleInputChange('container_id', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm font-mono border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-2 py-1 text-sm font-mono border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Container ID"
|
||||
/>
|
||||
) : (
|
||||
script.container_id ? (
|
||||
<span className="text-sm font-mono text-gray-900 dark:text-gray-100">{String(script.container_id)}</span>
|
||||
<span className="text-sm font-mono text-foreground">{String(script.container_id)}</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500">-</span>
|
||||
<span className="text-sm text-muted-foreground">-</span>
|
||||
)
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{script.execution_mode === 'local' ? (
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">Local</span>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{script.server_name}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">{script.server_ip}</div>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<ExecutionModeBadge mode={script.execution_mode}>
|
||||
{script.execution_mode.toUpperCase()}
|
||||
</ExecutionModeBadge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{script.server_name ?? 'Local'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<StatusBadge status={script.status}>
|
||||
{script.status.replace('_', ' ').toUpperCase()}
|
||||
</StatusBadge>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
|
||||
{formatDate(String(script.installation_date))}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
{editingScriptId === script.id ? (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleSaveEdit}
|
||||
disabled={updateScriptMutation.isPending}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm font-medium disabled:opacity-50"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
{updateScriptMutation.isPending ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelEdit}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white px-3 py-1 rounded text-sm font-medium"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleEditScript(script)}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm font-medium"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</Button>
|
||||
{script.container_id && (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleUpdateScript(script)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
variant="link"
|
||||
size="sm"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleDeleteScript(Number(script.id))}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={deleteScriptMutation.isPending}
|
||||
>
|
||||
{deleteScriptMutation.isPending ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '~/trpc/react';
|
||||
|
||||
interface ProxmoxCheckProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ProxmoxCheck({ children }: ProxmoxCheckProps) {
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
const [isProxmoxVE, setIsProxmoxVE] = useState<boolean | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { data: proxmoxData, isLoading } = api.scripts.checkProxmoxVE.useQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (proxmoxData && typeof proxmoxData === 'object' && 'success' in proxmoxData) {
|
||||
setIsChecking(false);
|
||||
if (proxmoxData.success) {
|
||||
const isProxmox = 'isProxmoxVE' in proxmoxData ? proxmoxData.isProxmoxVE as boolean : false;
|
||||
setIsProxmoxVE(isProxmox);
|
||||
if (!isProxmox) {
|
||||
setError('This application can only run on a Proxmox VE Host');
|
||||
}
|
||||
} else {
|
||||
const errorMsg = 'error' in proxmoxData ? proxmoxData.error as string : 'Failed to check Proxmox VE status';
|
||||
setError(errorMsg);
|
||||
setIsProxmoxVE(false);
|
||||
}
|
||||
}
|
||||
}, [proxmoxData]);
|
||||
|
||||
// Show loading state
|
||||
if (isChecking || isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Checking system requirements...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error if not running on Proxmox VE
|
||||
if (!isProxmoxVE || error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<div className="max-w-md mx-auto text-center">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-8">
|
||||
<div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full">
|
||||
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 19.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-red-800 mb-2">
|
||||
System Requirements Not Met
|
||||
</h1>
|
||||
<p className="text-red-700 mb-4">
|
||||
{error ?? 'This application can only run on a Proxmox VE Host'}
|
||||
</p>
|
||||
<div className="text-sm text-red-600 bg-red-100 rounded-lg p-4">
|
||||
<p className="font-medium mb-2">To use this application, you need:</p>
|
||||
<ul className="text-left space-y-1">
|
||||
<li>• A Proxmox VE host system</li>
|
||||
<li>• The <code className="bg-red-200 px-1 rounded">pveversion</code> command must be available</li>
|
||||
<li>• Proper permissions to execute system commands</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-6 px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Retry Check
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If running on Proxmox VE, render the children
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { api } from '~/trpc/react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function ResyncButton() {
|
||||
const [isResyncing, setIsResyncing] = useState(false);
|
||||
@@ -39,36 +40,34 @@ export function ResyncButton() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 font-medium">
|
||||
<div className="text-sm text-muted-foreground font-medium">
|
||||
Sync scripts with ProxmoxVE repo
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<button
|
||||
<Button
|
||||
onClick={handleResync}
|
||||
disabled={isResyncing}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
isResyncing
|
||||
? 'bg-gray-400 dark:bg-gray-600 text-white cursor-not-allowed'
|
||||
: 'bg-blue-600 dark:bg-blue-700 text-white hover:bg-blue-700 dark:hover:bg-blue-600'
|
||||
}`}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="inline-flex items-center"
|
||||
>
|
||||
{isResyncing ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
<span>Syncing...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Sync Json Files</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{lastSync && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Last sync: {lastSync.toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
@@ -77,8 +76,8 @@ export function ResyncButton() {
|
||||
{syncMessage && (
|
||||
<div className={`text-sm px-3 py-1 rounded-lg ${
|
||||
syncMessage.includes('Error') || syncMessage.includes('Failed')
|
||||
? 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300'
|
||||
: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'
|
||||
? 'bg-red-100 text-destructive'
|
||||
: 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{syncMessage}
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg dark:hover:shadow-xl transition-shadow duration-200 cursor-pointer border border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600 h-full flex flex-col"
|
||||
className="bg-card rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer border border-border hover:border-primary h-full flex flex-col"
|
||||
onClick={() => onClick(script)}
|
||||
>
|
||||
<div className="p-6 flex-1 flex flex-col">
|
||||
@@ -36,15 +36,15 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
|
||||
onError={handleImageError}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
||||
<span className="text-gray-500 dark:text-gray-400 text-lg font-semibold">
|
||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center">
|
||||
<span className="text-muted-foreground text-lg font-semibold">
|
||||
{script.name?.charAt(0)?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
<h3 className="text-lg font-semibold text-foreground truncate">
|
||||
{script.name || 'Unnamed Script'}
|
||||
</h3>
|
||||
<div className="mt-2 space-y-2">
|
||||
@@ -60,7 +60,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
|
||||
script.isDownloaded ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<span className={`text-xs font-medium ${
|
||||
script.isDownloaded ? 'text-green-700 dark:text-green-300' : 'text-red-700 dark:text-red-300'
|
||||
script.isDownloaded ? 'text-green-700' : 'text-destructive'
|
||||
}`}>
|
||||
{script.isDownloaded ? 'Downloaded' : 'Not Downloaded'}
|
||||
</span>
|
||||
@@ -70,7 +70,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm line-clamp-3 mb-4 flex-1">
|
||||
<p className="text-muted-foreground text-sm line-clamp-3 mb-4 flex-1">
|
||||
{script.description || 'No description available'}
|
||||
</p>
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
|
||||
href={script.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-sm font-medium flex items-center space-x-1"
|
||||
className="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center space-x-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>Website</span>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DiffViewer } from "./DiffViewer";
|
||||
import { TextViewer } from "./TextViewer";
|
||||
import { ExecutionModeModal } from "./ExecutionModeModal";
|
||||
import { TypeBadge, UpdateableBadge, PrivilegedBadge, NoteBadge } from "./Badge";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface ScriptDetailModalProps {
|
||||
script: Script | null;
|
||||
@@ -132,12 +133,12 @@ export function ScriptDetailModal({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black p-4"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-sm bg-black/50"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-6xl w-full max-h-[95vh] min-h-[80vh] overflow-y-auto">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-6xl w-full max-h-[95vh] min-h-[80vh] overflow-y-auto border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between border-b border-border p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
{script.logo && !imageError ? (
|
||||
<Image
|
||||
@@ -149,14 +150,14 @@ export function ScriptDetailModal({
|
||||
onError={handleImageError}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-gray-200 dark:bg-gray-700">
|
||||
<span className="text-2xl font-semibold text-gray-500 dark:text-gray-400">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-muted">
|
||||
<span className="text-2xl font-semibold text-muted-foreground">
|
||||
{script.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
{script.name}
|
||||
</h2>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
@@ -171,9 +172,11 @@ export function ScriptDetailModal({
|
||||
{scriptFilesData?.success &&
|
||||
scriptFilesData.ctExists &&
|
||||
onInstallScript && (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleInstallScript}
|
||||
className="flex items-center space-x-2 rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700"
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
@@ -189,15 +192,17 @@ export function ScriptDetailModal({
|
||||
/>
|
||||
</svg>
|
||||
<span>Install</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* View Button - only show if script files exist */}
|
||||
{scriptFilesData?.success &&
|
||||
(scriptFilesData.ctExists || scriptFilesData.installExists) && (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleViewScript}
|
||||
className="flex items-center space-x-2 rounded-lg bg-purple-600 px-4 py-2 font-medium text-white transition-colors hover:bg-purple-700"
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex items-center space-x-2 "
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
@@ -219,7 +224,7 @@ export function ScriptDetailModal({
|
||||
/>
|
||||
</svg>
|
||||
<span>View</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Load/Update Script Button */}
|
||||
@@ -239,7 +244,7 @@ export function ScriptDetailModal({
|
||||
disabled={isLoading}
|
||||
className={`flex items-center space-x-2 rounded-lg px-4 py-2 font-medium transition-colors ${
|
||||
isLoading
|
||||
? "cursor-not-allowed bg-gray-400 text-white"
|
||||
? "cursor-not-allowed bg-muted text-muted-foreground"
|
||||
: "bg-green-600 text-white hover:bg-green-700"
|
||||
}`}
|
||||
>
|
||||
@@ -273,7 +278,7 @@ export function ScriptDetailModal({
|
||||
return (
|
||||
<button
|
||||
disabled
|
||||
className="flex cursor-not-allowed items-center space-x-2 rounded-lg bg-gray-400 px-4 py-2 font-medium text-white transition-colors"
|
||||
className="flex cursor-not-allowed items-center space-x-2 rounded-lg bg-muted px-4 py-2 font-medium text-muted-foreground transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
@@ -299,7 +304,7 @@ export function ScriptDetailModal({
|
||||
disabled={isLoading}
|
||||
className={`flex items-center space-x-2 rounded-lg px-4 py-2 font-medium transition-colors ${
|
||||
isLoading
|
||||
? "cursor-not-allowed bg-gray-400 text-white"
|
||||
? "cursor-not-allowed bg-muted text-muted-foreground"
|
||||
: "bg-orange-600 text-white hover:bg-orange-700"
|
||||
}`}
|
||||
>
|
||||
@@ -330,9 +335,11 @@ export function ScriptDetailModal({
|
||||
);
|
||||
}
|
||||
})()}
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
@@ -347,20 +354,20 @@ export function ScriptDetailModal({
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Load Message */}
|
||||
{loadMessage && (
|
||||
<div className="mx-6 mb-4 rounded-lg bg-blue-50 p-3 text-sm text-blue-800">
|
||||
<div className="mx-6 mb-4 rounded-lg bg-primary/10 p-3 text-sm text-primary">
|
||||
{loadMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Script Files Status */}
|
||||
{(scriptFilesLoading || comparisonLoading) && (
|
||||
<div className="mx-6 mb-4 rounded-lg bg-blue-50 p-3 text-sm">
|
||||
<div className="mx-6 mb-4 rounded-lg bg-primary/10 p-3 text-sm text-primary">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||
<span>Loading script status...</span>
|
||||
@@ -385,11 +392,11 @@ export function ScriptDetailModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-6 mb-4 rounded-lg bg-gray-50 p-3 text-sm text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
||||
<div className="mx-6 mb-4 rounded-lg bg-muted p-3 text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${scriptFilesData.ctExists ? "bg-green-500" : "bg-gray-300"}`}
|
||||
className={`h-2 w-2 rounded-full ${scriptFilesData.ctExists ? "bg-green-500" : "bg-muted"}`}
|
||||
></div>
|
||||
<span>
|
||||
{scriptType}:{" "}
|
||||
@@ -398,7 +405,7 @@ export function ScriptDetailModal({
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${scriptFilesData.installExists ? "bg-green-500" : "bg-gray-300"}`}
|
||||
className={`h-2 w-2 rounded-full ${scriptFilesData.installExists ? "bg-green-500" : "bg-muted"}`}
|
||||
></div>
|
||||
<span>
|
||||
Install Script:{" "}
|
||||
@@ -426,7 +433,7 @@ export function ScriptDetailModal({
|
||||
)}
|
||||
</div>
|
||||
{scriptFilesData.files.length > 0 && (
|
||||
<div className="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Files: {scriptFilesData.files.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
@@ -438,10 +445,10 @@ export function ScriptDetailModal({
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Description */}
|
||||
<div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="mb-2 text-lg font-semibold text-foreground">
|
||||
Description
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
<p className="text-muted-foreground">
|
||||
{script.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -449,50 +456,50 @@ export function ScriptDetailModal({
|
||||
{/* Basic Information */}
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="mb-3 text-lg font-semibold text-foreground">
|
||||
Basic Information
|
||||
</h3>
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Slug
|
||||
</dt>
|
||||
<dd className="font-mono text-sm text-gray-900 dark:text-gray-100">
|
||||
<dd className="font-mono text-sm text-foreground">
|
||||
{script.slug}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Date Created
|
||||
</dt>
|
||||
<dd className="text-sm text-gray-900 dark:text-gray-100">
|
||||
<dd className="text-sm text-foreground">
|
||||
{script.date_created}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Categories
|
||||
</dt>
|
||||
<dd className="text-sm text-gray-900 dark:text-gray-100">
|
||||
<dd className="text-sm text-foreground">
|
||||
{script.categories.join(", ")}
|
||||
</dd>
|
||||
</div>
|
||||
{script.interface_port && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Interface Port
|
||||
</dt>
|
||||
<dd className="text-sm text-gray-900 dark:text-gray-100">
|
||||
<dd className="text-sm text-foreground">
|
||||
{script.interface_port}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{script.config_path && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Config Path
|
||||
</dt>
|
||||
<dd className="font-mono text-sm text-gray-900 dark:text-gray-100">
|
||||
<dd className="font-mono text-sm text-foreground">
|
||||
{script.config_path}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -501,13 +508,13 @@ export function ScriptDetailModal({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="mb-3 text-lg font-semibold text-foreground">
|
||||
Links
|
||||
</h3>
|
||||
<dl className="space-y-2">
|
||||
{script.website && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Website
|
||||
</dt>
|
||||
<dd className="text-sm">
|
||||
@@ -515,7 +522,7 @@ export function ScriptDetailModal({
|
||||
href={script.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
className="break-all text-primary hover:text-primary/80"
|
||||
>
|
||||
{script.website}
|
||||
</a>
|
||||
@@ -524,7 +531,7 @@ export function ScriptDetailModal({
|
||||
)}
|
||||
{script.documentation && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Documentation
|
||||
</dt>
|
||||
<dd className="text-sm">
|
||||
@@ -532,7 +539,7 @@ export function ScriptDetailModal({
|
||||
href={script.documentation}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
className="break-all text-primary hover:text-primary/80"
|
||||
>
|
||||
{script.documentation}
|
||||
</a>
|
||||
@@ -548,53 +555,53 @@ export function ScriptDetailModal({
|
||||
script.type !== "pve" &&
|
||||
script.type !== "addon" && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="mb-3 text-lg font-semibold text-foreground">
|
||||
Install Methods
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{script.install_methods.map((method, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="rounded-lg border border-border bg-card p-4"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h4 className="font-medium text-gray-900 capitalize dark:text-gray-100">
|
||||
<h4 className="font-medium text-foreground capitalize">
|
||||
{method.type}
|
||||
</h4>
|
||||
<span className="font-mono text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-mono text-sm text-muted-foreground">
|
||||
{method.script}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm md:grid-cols-4">
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="font-medium text-muted-foreground">
|
||||
CPU
|
||||
</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">
|
||||
<dd className="text-foreground">
|
||||
{method.resources.cpu} cores
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="font-medium text-muted-foreground">
|
||||
RAM
|
||||
</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">
|
||||
<dd className="text-foreground">
|
||||
{method.resources.ram} MB
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="font-medium text-muted-foreground">
|
||||
HDD
|
||||
</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">
|
||||
<dd className="text-foreground">
|
||||
{method.resources.hdd} GB
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">
|
||||
<dt className="font-medium text-muted-foreground">
|
||||
OS
|
||||
</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">
|
||||
<dd className="text-foreground">
|
||||
{method.resources.os} {method.resources.version}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -609,26 +616,26 @@ export function ScriptDetailModal({
|
||||
{(script.default_credentials.username ??
|
||||
script.default_credentials.password) && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-lg font-semibold text-gray-900">
|
||||
<h3 className="mb-3 text-lg font-semibold text-foreground">
|
||||
Default Credentials
|
||||
</h3>
|
||||
<dl className="space-y-2">
|
||||
{script.default_credentials.username && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Username
|
||||
</dt>
|
||||
<dd className="font-mono text-sm text-gray-900">
|
||||
<dd className="font-mono text-sm text-foreground">
|
||||
{script.default_credentials.username}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{script.default_credentials.password && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Password
|
||||
</dt>
|
||||
<dd className="font-mono text-sm text-gray-900">
|
||||
<dd className="font-mono text-sm text-foreground">
|
||||
{script.default_credentials.password}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -640,7 +647,7 @@ export function ScriptDetailModal({
|
||||
{/* Notes */}
|
||||
{script.notes.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="mb-3 text-lg font-semibold text-foreground">
|
||||
Notes
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
@@ -655,10 +662,10 @@ export function ScriptDetailModal({
|
||||
key={index}
|
||||
className={`rounded-lg p-3 text-sm ${
|
||||
noteType === "warning"
|
||||
? "border-l-4 border-yellow-400 bg-yellow-50 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-200"
|
||||
? "border-l-4 border-yellow-400 bg-yellow-500/10 text-yellow-400"
|
||||
: noteType === "error"
|
||||
? "border-l-4 border-red-400 bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-200"
|
||||
: "bg-gray-50 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||
? "border-l-4 border-destructive bg-destructive/10 text-destructive"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ScriptCard } from './ScriptCard';
|
||||
import { ScriptDetailModal } from './ScriptDetailModal';
|
||||
import { CategorySidebar } from './CategorySidebar';
|
||||
import { FilterBar, type FilterState } from './FilterBar';
|
||||
import { Button } from './ui/button';
|
||||
import type { ScriptCard as ScriptCardType } from '~/types/script';
|
||||
|
||||
|
||||
@@ -269,7 +270,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-2 text-gray-600">Loading scripts...</span>
|
||||
<span className="ml-2 text-muted-foreground">Loading scripts...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -282,16 +283,18 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium">Failed to load scripts</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{githubError?.message ?? localError?.message ?? 'Unknown error occurred'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
variant="default"
|
||||
size="default"
|
||||
className="mt-4"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -299,12 +302,12 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
if (!scriptsWithStatus || scriptsWithStatus.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500">
|
||||
<div className="text-muted-foreground">
|
||||
<svg className="w-12 h-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium">No scripts found</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
No script files were found in the repository or local directory.
|
||||
</p>
|
||||
</div>
|
||||
@@ -340,7 +343,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
<div className="hidden mb-8">
|
||||
<div className="relative max-w-md mx-auto">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="h-5 w-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -349,12 +352,12 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
placeholder="Search scripts by name..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg leading-5 bg-white dark:bg-gray-800 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-gray-100 focus:outline-none focus:placeholder-gray-400 dark:focus:placeholder-gray-300 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 text-sm"
|
||||
className="block w-full pl-10 pr-3 py-3 border border-border rounded-lg leading-5 bg-card placeholder-muted-foreground text-foreground focus:outline-none focus:placeholder-muted-foreground focus:ring-2 focus:ring-ring focus:border-ring text-sm"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -363,7 +366,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
)}
|
||||
</div>
|
||||
{(searchQuery || selectedCategory) && (
|
||||
<div className="text-center mt-2 text-sm text-gray-600">
|
||||
<div className="text-center mt-2 text-sm text-muted-foreground">
|
||||
{filteredScripts.length === 0 ? (
|
||||
<span>No scripts found{searchQuery ? ` matching "${searchQuery}"` : ''}{selectedCategory ? ` in category "${selectedCategory}"` : ''}</span>
|
||||
) : (
|
||||
@@ -380,30 +383,32 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
{/* Scripts Grid */}
|
||||
{filteredScripts.length === 0 && (filters.searchQuery || selectedCategory || filters.showUpdatable !== null || filters.selectedTypes.length > 0) ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500">
|
||||
<div className="text-muted-foreground">
|
||||
<svg className="w-12 h-12 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium">No matching scripts found</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Try different filter settings or clear all filters.
|
||||
</p>
|
||||
<div className="flex justify-center gap-2 mt-4">
|
||||
{filters.searchQuery && (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleFiltersChange({ ...filters, searchQuery: '' })}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
variant="default"
|
||||
size="default"
|
||||
>
|
||||
Clear Search
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{selectedCategory && (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleCategorySelect(null)}
|
||||
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||
variant="secondary"
|
||||
size="default"
|
||||
>
|
||||
Clear Category
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { CreateServerData } from '../../types/server';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface ServerFormProps {
|
||||
onSubmit: (data: CreateServerData) => void;
|
||||
@@ -75,7 +76,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-muted-foreground mb-1">
|
||||
Server Name *
|
||||
</label>
|
||||
<input
|
||||
@@ -83,16 +84,16 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange('name')}
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 ${
|
||||
errors.name ? 'border-red-300 dark:border-red-600' : 'border-gray-300 dark:border-gray-600'
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
|
||||
errors.name ? 'border-destructive' : 'border-border'
|
||||
}`}
|
||||
placeholder="e.g., Production Server"
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.name}</p>}
|
||||
{errors.name && <p className="mt-1 text-sm text-destructive">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="ip" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label htmlFor="ip" className="block text-sm font-medium text-muted-foreground mb-1">
|
||||
IP Address *
|
||||
</label>
|
||||
<input
|
||||
@@ -100,16 +101,16 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
id="ip"
|
||||
value={formData.ip}
|
||||
onChange={handleChange('ip')}
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 ${
|
||||
errors.ip ? 'border-red-300 dark:border-red-600' : 'border-gray-300 dark:border-gray-600'
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
|
||||
errors.ip ? 'border-destructive' : 'border-border'
|
||||
}`}
|
||||
placeholder="e.g., 192.168.1.100"
|
||||
/>
|
||||
{errors.ip && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.ip}</p>}
|
||||
{errors.ip && <p className="mt-1 text-sm text-destructive">{errors.ip}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="user" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label htmlFor="user" className="block text-sm font-medium text-muted-foreground mb-1">
|
||||
Username *
|
||||
</label>
|
||||
<input
|
||||
@@ -117,16 +118,16 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
id="user"
|
||||
value={formData.user}
|
||||
onChange={handleChange('user')}
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 ${
|
||||
errors.user ? 'border-red-300 dark:border-red-600' : 'border-gray-300 dark:border-gray-600'
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
|
||||
errors.user ? 'border-destructive' : 'border-border'
|
||||
}`}
|
||||
placeholder="e.g., root"
|
||||
/>
|
||||
{errors.user && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.user}</p>}
|
||||
{errors.user && <p className="mt-1 text-sm text-destructive">{errors.user}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-muted-foreground mb-1">
|
||||
Password *
|
||||
</label>
|
||||
<input
|
||||
@@ -134,31 +135,33 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
id="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange('password')}
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 ${
|
||||
errors.password ? 'border-red-300 dark:border-red-600' : 'border-gray-300 dark:border-gray-600'
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
|
||||
errors.password ? 'border-destructive' : 'border-border'
|
||||
}`}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
{errors.password && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.password}</p>}
|
||||
{errors.password && <p className="mt-1 text-sm text-destructive">{errors.password}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
{isEditing && onCancel && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 dark:bg-blue-700 border border-transparent rounded-md hover:bg-blue-700 dark:hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
variant="default"
|
||||
size="default"
|
||||
>
|
||||
{isEditing ? 'Update Server' : 'Add Server'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import type { Server, CreateServerData } from '../../types/server';
|
||||
import { ServerForm } from './ServerForm';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface ServerListProps {
|
||||
servers: Server[];
|
||||
@@ -71,12 +72,12 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
|
||||
if (servers.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<svg className="mx-auto h-12 w-12 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No servers configured</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Get started by adding a new server configuration above.</p>
|
||||
<h3 className="mt-2 text-sm font-medium text-foreground">No servers configured</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Get started by adding a new server configuration above.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -84,10 +85,10 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{servers.map((server) => (
|
||||
<div key={server.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||
<div key={server.id} className="bg-card border border-border rounded-lg p-4 shadow-sm">
|
||||
{editingId === server.id ? (
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-gray-900 mb-4">Edit Server</h4>
|
||||
<h4 className="text-lg font-medium text-foreground mb-4">Edit Server</h4>
|
||||
<ServerForm
|
||||
initialData={{
|
||||
name: server.name,
|
||||
@@ -112,8 +113,8 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-medium text-gray-900 truncate">{server.name}</h3>
|
||||
<div className="mt-1 flex items-center space-x-4 text-sm text-gray-500">
|
||||
<h3 className="text-lg font-medium text-foreground truncate">{server.name}</h3>
|
||||
<div className="mt-1 flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
|
||||
@@ -127,7 +128,7 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
{server.user}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-400">
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
Created: {new Date(server.created_at).toLocaleDateString()}
|
||||
{server.updated_at !== server.created_at && (
|
||||
<span> • Updated: {new Date(server.updated_at).toLocaleDateString()}</span>
|
||||
@@ -162,10 +163,12 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleTestConnection(server)}
|
||||
disabled={testingConnections.has(server.id)}
|
||||
className="inline-flex items-center px-3 py-1.5 border border-green-300 text-xs font-medium rounded text-green-700 bg-white hover:bg-green-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-green-500/20 text-green-400 bg-green-500/10 hover:bg-green-500/20"
|
||||
>
|
||||
{testingConnections.has(server.id) ? (
|
||||
<>
|
||||
@@ -182,25 +185,28 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
Test Connection
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleEdit(server)}
|
||||
className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDelete(server.id)}
|
||||
className="inline-flex items-center px-3 py-1.5 border border-red-300 text-xs font-medium rounded text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-destructive/20 text-destructive bg-destructive/10 hover:bg-destructive/20"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SettingsModal } from './SettingsModal';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function SettingsButton() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -9,12 +10,14 @@ export function SettingsButton() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 font-medium">
|
||||
Add and manage PVE Servers
|
||||
<div className="text-sm text-muted-foreground font-medium">
|
||||
Add and manage PVE Servers:
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 focus:ring-blue-500 dark:focus:ring-blue-400 transition-colors duration-200"
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="inline-flex items-center"
|
||||
title="Add PVE Server"
|
||||
>
|
||||
<svg
|
||||
@@ -38,7 +41,7 @@ export function SettingsButton() {
|
||||
/>
|
||||
</svg>
|
||||
Manage PVE Servers
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SettingsModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import type { Server, CreateServerData } from '../../types/server';
|
||||
import { ServerForm } from './ServerForm';
|
||||
import { ServerList } from './ServerList';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -98,54 +99,60 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 dark:bg-black dark:bg-opacity-70 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Settings</h2>
|
||||
<button
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<h2 className="text-2xl font-bold text-card-foreground">Settings</h2>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex space-x-8 px-6">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setActiveTab('servers')}
|
||||
variant="ghost"
|
||||
size="null"
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'servers'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
Server Settings
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveTab('general')}
|
||||
variant="ghost"
|
||||
size="null"
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'general'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
General
|
||||
</button>
|
||||
</Button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
|
||||
<div className="mb-4 p-4 bg-destructive/10 border border-destructive rounded-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400 dark:text-red-300" viewBox="0 0 20 20" fill="currentColor">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -160,14 +167,14 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
||||
{activeTab === 'servers' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Server Configurations</h3>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">Server Configurations</h3>
|
||||
<ServerForm onSubmit={handleCreateServer} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Saved Servers</h3>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">Saved Servers</h3>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-2 text-gray-600">Loading servers...</p>
|
||||
</div>
|
||||
@@ -184,8 +191,8 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
||||
|
||||
{activeTab === 'general' && (
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">General Settings</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">General settings will be available in a future update.</p>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">General Settings</h3>
|
||||
<p className="text-muted-foreground">General settings will be available in a future update.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface TerminalProps {
|
||||
scriptPath: string;
|
||||
@@ -57,7 +58,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
cursor: '#00ff00',
|
||||
},
|
||||
fontSize: 14,
|
||||
fontFamily: 'Courier New, monospace',
|
||||
fontFamily: 'JetBrains Mono, Fira Code, Cascadia Code, Monaco, Menlo, Ubuntu Mono, monospace',
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'block',
|
||||
scrollback: 1000,
|
||||
@@ -276,44 +277,44 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
// Don't render on server side
|
||||
if (!isClient) {
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-b border-gray-700">
|
||||
<div className="bg-card rounded-lg border border-border overflow-hidden">
|
||||
<div className="bg-muted px-4 py-2 flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
<span className="text-gray-300 font-mono text-sm ml-2">
|
||||
<span className="text-foreground font-mono text-sm ml-2">
|
||||
{scriptName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-96 w-full flex items-center justify-center">
|
||||
<div className="text-gray-400">Loading terminal...</div>
|
||||
<div className="text-muted-foreground">Loading terminal...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div className="bg-card rounded-lg border border-border overflow-hidden">
|
||||
{/* Terminal Header */}
|
||||
<div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-b border-gray-700">
|
||||
<div className="bg-muted px-4 py-2 flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
<span className="text-gray-300 font-mono text-sm ml-2">
|
||||
<span className="text-foreground font-mono text-sm ml-2">
|
||||
{scriptName} {mode === 'ssh' && server && `(SSH: ${server.name})`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
<span className="text-gray-400 text-xs">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -322,51 +323,51 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
{/* Terminal Output */}
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className="h-96 w-full"
|
||||
style={{ minHeight: '384px' }}
|
||||
className="h-[32rem] w-full max-w-4xl mx-auto"
|
||||
style={{ minHeight: '512px' }}
|
||||
/>
|
||||
|
||||
{/* Terminal Controls */}
|
||||
<div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-t border-gray-700">
|
||||
<div className="bg-muted px-4 py-2 flex items-center justify-between border-t border-border">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={startScript}
|
||||
disabled={!isConnected || isRunning}
|
||||
className={`px-3 py-1 text-xs font-medium rounded transition-colors ${
|
||||
isConnected && !isRunning
|
||||
? 'bg-green-600 text-white hover:bg-green-700'
|
||||
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className={isConnected && !isRunning ? 'bg-green-600 hover:bg-green-700' : 'bg-muted text-muted-foreground cursor-not-allowed'}
|
||||
>
|
||||
▶️ Start
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={stopScript}
|
||||
disabled={!isRunning}
|
||||
className={`px-3 py-1 text-xs font-medium rounded transition-colors ${
|
||||
isRunning
|
||||
? 'bg-red-600 text-white hover:bg-red-700'
|
||||
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className={isRunning ? 'bg-red-600 hover:bg-red-700' : 'bg-muted text-muted-foreground cursor-not-allowed'}
|
||||
>
|
||||
⏹️ Stop
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={clearOutput}
|
||||
className="px-3 py-1 text-xs font-medium bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="bg-secondary text-secondary-foreground hover:bg-secondary/80"
|
||||
>
|
||||
🗑️ Clear
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-xs font-medium bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="bg-gray-600 text-white hover:bg-gray-700"
|
||||
>
|
||||
✕ Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface TextViewerProps {
|
||||
scriptName: string;
|
||||
@@ -99,44 +100,38 @@ export function TextViewer({ scriptName, isOpen, onClose }: TextViewerProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4 z-50"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] flex flex-col">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] flex flex-col border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h2 className="text-2xl font-bold text-gray-800">
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
Script Viewer: {scriptName}
|
||||
</h2>
|
||||
{scriptContent.ctScript && scriptContent.installScript && (
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
<Button
|
||||
variant={activeTab === 'ct' ? 'outline' : 'ghost'}
|
||||
onClick={() => setActiveTab('ct')}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${
|
||||
activeTab === 'ct'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
className="px-3 py-1 text-sm"
|
||||
>
|
||||
CT Script
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'install' ? 'outline' : 'ghost'}
|
||||
onClick={() => setActiveTab('install')}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${
|
||||
activeTab === 'install'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
className="px-3 py-1 text-sm"
|
||||
>
|
||||
Install Script
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -148,11 +143,11 @@ export function TextViewer({ scriptName, isOpen, onClose }: TextViewerProps) {
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-lg text-gray-600">Loading script content...</div>
|
||||
<div className="text-lg text-muted-foreground">Loading script content...</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-lg text-red-600">Error: {error}</div>
|
||||
<div className="text-lg text-destructive">Error: {error}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
||||
109
src/app/_components/ui/button.tsx
Normal file
109
src/app/_components/ui/button.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
import { Slot, Slottable } from "@radix-ui/react-slot";
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
expandIcon:
|
||||
"group relative text-primary-foreground bg-primary hover:bg-primary/90",
|
||||
ringHover:
|
||||
"bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2",
|
||||
shine:
|
||||
"text-primary-foreground animate-shine bg-gradient-to-r from-primary via-primary/75 to-primary bg-[length:400%_100%] ",
|
||||
gooeyRight:
|
||||
"text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-zinc-400 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%] ",
|
||||
gooeyLeft:
|
||||
"text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-zinc-400 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%] ",
|
||||
linkHover1:
|
||||
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300",
|
||||
linkHover2:
|
||||
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-9 w-9 ",
|
||||
null: "py-1 px-3 rouded-xs",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type IconProps = {
|
||||
Icon: React.ElementType;
|
||||
iconPlacement: "left" | "right";
|
||||
};
|
||||
|
||||
type IconRefProps = {
|
||||
Icon?: never;
|
||||
iconPlacement?: undefined;
|
||||
};
|
||||
|
||||
export type ButtonProps = {
|
||||
asChild?: boolean;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||
|
||||
export type ButtonIconProps = IconProps | IconRefProps;
|
||||
|
||||
const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps & ButtonIconProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
Icon,
|
||||
iconPlacement,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{Icon && iconPlacement === "left" && (
|
||||
<div className="group-hover:translate-x-100 w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:pr-2 group-hover:opacity-100">
|
||||
<Icon />
|
||||
</div>
|
||||
)}
|
||||
<Slottable>{props.children}</Slottable>
|
||||
{Icon && iconPlacement === "right" && (
|
||||
<div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100">
|
||||
<Icon />
|
||||
</div>
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -4,8 +4,6 @@ import { type Metadata } from "next";
|
||||
import { Geist } from "next/font/google";
|
||||
|
||||
import { TRPCReactProvider } from "~/trpc/react";
|
||||
import { DarkModeProvider } from "./_components/DarkModeProvider";
|
||||
import { DarkModeToggle } from "./_components/DarkModeToggle";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PVE Scripts local",
|
||||
@@ -19,54 +17,29 @@ export const metadata: Metadata = {
|
||||
|
||||
const geist = Geist({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-sans",
|
||||
variable: "--font-jetbrains-mono",
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable}`}>
|
||||
<html lang="en" className={`${geist.variable} dark`}>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
const stored = localStorage.getItem('theme');
|
||||
const theme = stored && ['light', 'dark', 'system'].includes(stored) ? stored : 'system';
|
||||
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const shouldBeDark = theme === 'dark' || (theme === 'system' && systemDark);
|
||||
|
||||
if (shouldBeDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to system preference if localStorage fails
|
||||
const systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (systemDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
})();
|
||||
// Force dark mode
|
||||
document.documentElement.classList.add('dark');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors"
|
||||
className="bg-background text-foreground transition-colors"
|
||||
suppressHydrationWarning={true}
|
||||
>
|
||||
<DarkModeProvider>
|
||||
{/* Dark Mode Toggle in top right corner */}
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
</DarkModeProvider>
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { InstalledScriptsTab } from './_components/InstalledScriptsTab';
|
||||
import { ResyncButton } from './_components/ResyncButton';
|
||||
import { Terminal } from './_components/Terminal';
|
||||
import { SettingsButton } from './_components/SettingsButton';
|
||||
import { Button } from './_components/ui/button';
|
||||
|
||||
export default function Home() {
|
||||
const [runningScript, setRunningScript] = useState<{ path: string; name: string; mode?: 'local' | 'ssh'; server?: any } | null>(null);
|
||||
@@ -21,21 +22,21 @@ export default function Home() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="min-h-screen bg-background">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-800 dark:text-gray-100 mb-2">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-2">
|
||||
🚀 PVE Scripts Management
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
<p className="text-muted-foreground">
|
||||
Manage and execute Proxmox helper scripts locally with live output streaming
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 p-6 bg-card rounded-lg shadow-sm border border-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<SettingsButton />
|
||||
</div>
|
||||
@@ -47,28 +48,30 @@ export default function Home() {
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="mb-8">
|
||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="border-b border-border">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('scripts')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
className={`px-3 py-1 text-sm ${
|
||||
activeTab === 'scripts'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent hover:text-accent-foreground'
|
||||
}`}>
|
||||
📦 Available Scripts
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('installed')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
className={`px-3 py-1 text-sm ${
|
||||
activeTab === 'installed'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent hover:text-accent-foreground'
|
||||
}`}>
|
||||
🗂️ Installed Scripts
|
||||
</button>
|
||||
</Button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -1,11 +1,130 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--primary: 220.9 39.3% 11%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 224 71.4% 4.1%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 210 20% 98%;
|
||||
--primary-foreground: 220.9 39.3% 11%;
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--ring: 216 12.2% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@variant dark (&:is(.dark, .dark *));
|
||||
/* Semantic color utility classes */
|
||||
.bg-background { background-color: hsl(var(--background)); }
|
||||
.text-foreground { color: hsl(var(--foreground)); }
|
||||
.bg-card { background-color: hsl(var(--card)); }
|
||||
.text-card-foreground { color: hsl(var(--card-foreground)); }
|
||||
.bg-popover { background-color: hsl(var(--popover)); }
|
||||
.text-popover-foreground { color: hsl(var(--popover-foreground)); }
|
||||
.bg-primary { background-color: hsl(var(--primary)); }
|
||||
.text-primary { color: hsl(var(--primary)); }
|
||||
.text-primary-foreground { color: hsl(var(--primary-foreground)); }
|
||||
.bg-secondary { background-color: hsl(var(--secondary)); }
|
||||
.text-secondary { color: hsl(var(--secondary)); }
|
||||
.text-secondary-foreground { color: hsl(var(--secondary-foreground)); }
|
||||
.bg-muted { background-color: hsl(var(--muted)); }
|
||||
.text-muted-foreground { color: hsl(var(--muted-foreground)); }
|
||||
.bg-accent { background-color: hsl(var(--accent)); }
|
||||
.text-accent { color: hsl(var(--accent)); }
|
||||
.text-accent-foreground { color: hsl(var(--accent-foreground)); }
|
||||
.bg-destructive { background-color: hsl(var(--destructive)); }
|
||||
.text-destructive { color: hsl(var(--destructive)); }
|
||||
.text-destructive-foreground { color: hsl(var(--destructive-foreground)); }
|
||||
.border-border { border-color: hsl(var(--border)); }
|
||||
.border-input { border-color: hsl(var(--input)); }
|
||||
.ring-ring { --tw-ring-color: hsl(var(--ring)); }
|
||||
|
||||
/* Hover states for semantic colors */
|
||||
.hover\:bg-accent:hover { background-color: hsl(var(--accent)); }
|
||||
.hover\:text-accent-foreground:hover { color: hsl(var(--accent-foreground)); }
|
||||
.hover\:text-foreground:hover { color: hsl(var(--foreground)); }
|
||||
.hover\:bg-primary:hover { background-color: hsl(var(--primary)); }
|
||||
.hover\:bg-primary\/90:hover { background-color: hsl(var(--primary) / 0.9); }
|
||||
.hover\:bg-secondary:hover { background-color: hsl(var(--secondary)); }
|
||||
.hover\:bg-secondary\/80:hover { background-color: hsl(var(--secondary) / 0.8); }
|
||||
.hover\:bg-muted:hover { background-color: hsl(var(--muted)); }
|
||||
.hover\:text-primary:hover { color: hsl(var(--primary)); }
|
||||
.hover\:text-primary\/80:hover { color: hsl(var(--primary) / 0.8); }
|
||||
.hover\:border-primary:hover { border-color: hsl(var(--primary)); }
|
||||
.hover\:border-border:hover { border-color: hsl(var(--border)); }
|
||||
.hover\:ring-primary:hover { --tw-ring-color: hsl(var(--primary)); }
|
||||
.hover\:ring-2:hover { --tw-ring-width: 2px; }
|
||||
.hover\:ring-offset-2:hover { --tw-ring-offset-width: 2px; }
|
||||
|
||||
|
||||
* {
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.25);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.glass {
|
||||
backdrop-filter: blur(15px) saturate(100%);
|
||||
-webkit-backdrop-filter: blur(15px) saturate(100%);
|
||||
}
|
||||
|
||||
/* Terminal-specific styles for ANSI escape code rendering */
|
||||
.terminal-output {
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
"~/*": ["./src/*"],
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user