mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9be85c3da8 | ||
|
|
ea50dc349a | ||
|
|
d8c686177b | ||
|
|
d068daa4ea | ||
|
|
e7d6d481df | ||
|
|
368a8eb415 | ||
|
|
6377798d45 | ||
|
|
af87d09273 | ||
|
|
da3f0c7836 | ||
|
|
737e7cd7f7 | ||
|
|
a699feb4e2 | ||
|
|
b22b3679e4 | ||
|
|
895488d400 | ||
|
|
802b3003d5 | ||
|
|
3b3fd7f135 | ||
|
|
901a235de4 | ||
|
|
3caf6cca62 | ||
|
|
e284c95891 | ||
|
|
b0f2213b15 | ||
|
|
ee0d1a5d8e | ||
|
|
25816084aa | ||
|
|
87cc80b291 | ||
|
|
89222bed85 | ||
|
|
ad46f42ab1 | ||
|
|
34a9c77114 | ||
|
|
2c785fe35b | ||
|
|
1cdbfc674c | ||
|
|
9d93fb0893 | ||
|
|
556f0406aa | ||
|
|
0589465e4e | ||
|
|
ec3cc9cf18 | ||
|
|
4c235bb23f | ||
|
|
b414b85746 | ||
|
|
d049588f7f | ||
|
|
86161d66e2 |
No files matched your search
@@ -1,17 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 26.10.5
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Card grids in the Plugins tab and in plugins can show Auto (as many as fit) or a set number of cards per row
|
|
||||||
|
|
||||||
## 26.10.4
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Pin classic layout plugin (fixes proxmox install)
|
|
||||||
|
|
||||||
## 26.10.3
|
## 26.10.3
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
cask "termix" do
|
cask "termix" do
|
||||||
version "26.10.3"
|
version "26.10.2"
|
||||||
sha256 "a0c9db16f144c1e2be6ce2f4ead9268a010d12e0d0608feb34976750661a2239"
|
sha256 "9df1850b5f75a3c0a1b3876847d46b7b4ff993aa1e50da55f4c88819308ba223"
|
||||||
|
|
||||||
url "https://github.com/Termix-SSH/Termix/releases/download/v#{version}/termix_macos_universal_dmg.dmg"
|
url "https://github.com/Termix-SSH/Termix/releases/download/v#{version}/termix_macos_universal_dmg.dmg"
|
||||||
name "Termix"
|
name "Termix"
|
||||||
|
|||||||
@@ -66,9 +66,7 @@ Plugins add everything else. The official ones ship with Termix, and admins can
|
|||||||
| [Automations](https://github.com/Termix-SSH/Plugin-Automations) | Run steps on your hosts on a schedule or when something happens. |
|
| [Automations](https://github.com/Termix-SSH/Plugin-Automations) | Run steps on your hosts on a schedule or when something happens. |
|
||||||
| [Classic Layout](https://github.com/Termix-SSH/Plugin-Classic-Layout) | The classic Termix layout with an icon rail, sidebar, tab bar and dock. |
|
| [Classic Layout](https://github.com/Termix-SSH/Plugin-Classic-Layout) | The classic Termix layout with an icon rail, sidebar, tab bar and dock. |
|
||||||
| [Classic Themes](https://github.com/Termix-SSH/Plugin-Classic-Themes) | Light, dark and seven more color themes for Termix. |
|
| [Classic Themes](https://github.com/Termix-SSH/Plugin-Classic-Themes) | Light, dark and seven more color themes for Termix. |
|
||||||
| [Cloud Providers](https://github.com/Termix-SSH/Plugin-Cloud-Providers) | Add hosts from AWS, Azure, Google Cloud, DigitalOcean, Hetzner and other clouds, and keep them in sync. |
|
|
||||||
| [Credential Tools](https://github.com/Termix-SSH/Plugin-Credential-Tools) | Generate SSH keys, check them and copy a public key to a host. |
|
| [Credential Tools](https://github.com/Termix-SSH/Plugin-Credential-Tools) | Generate SSH keys, check them and copy a public key to a host. |
|
||||||
| [Databases](https://github.com/Termix-SSH/Plugin-Databases) | Browse and edit MySQL, MariaDB, PostgreSQL and SQLite databases on your hosts from one tab. |
|
|
||||||
| [Docker](https://github.com/Termix-SSH/Plugin-Docker) | Manage Docker and Podman containers on your hosts over SSH, with logs, stats and a console. |
|
| [Docker](https://github.com/Termix-SSH/Plugin-Docker) | Manage Docker and Podman containers on your hosts over SSH, with logs, stats and a console. |
|
||||||
| [File Manager](https://github.com/Termix-SSH/Plugin-File-Manager) | Browse, edit and move files over SFTP, including straight from one server to another. |
|
| [File Manager](https://github.com/Termix-SSH/Plugin-File-Manager) | Browse, edit and move files over SFTP, including straight from one server to another. |
|
||||||
| [Fleets](https://github.com/Termix-SSH/Plugin-Fleets) | Group hosts into fleets and run commands, package actions and file transfers on all of them at once. |
|
| [Fleets](https://github.com/Termix-SSH/Plugin-Fleets) | Group hosts into fleets and run commands, package actions and file transfers on all of them at once. |
|
||||||
@@ -78,7 +76,6 @@ Plugins add everything else. The official ones ship with Termix, and admins can
|
|||||||
| [Host Status](https://github.com/Termix-SSH/Plugin-Host-Status) | Shows if each host is online with a dot in the host list, checked by connecting to its port. |
|
| [Host Status](https://github.com/Termix-SSH/Plugin-Host-Status) | Shows if each host is online with a dot in the host list, checked by connecting to its port. |
|
||||||
| [Hosts Config](https://github.com/Termix-SSH/Plugin-Hosts-Config) | Keep hosts, folders and sharing in a YAML or JSON file. Termix loads them as read only hosts and reloads when the file changes. |
|
| [Hosts Config](https://github.com/Termix-SSH/Plugin-Hosts-Config) | Keep hosts, folders and sharing in a YAML or JSON file. Termix loads them as read only hosts and reloads when the file changes. |
|
||||||
| [Import and Export](https://github.com/Termix-SSH/Plugin-Import-Export) | Import hosts from a JSON file or an SSH config file and export them to share or back up. |
|
| [Import and Export](https://github.com/Termix-SSH/Plugin-Import-Export) | Import hosts from a JSON file or an SSH config file and export them to share or back up. |
|
||||||
| [Kubernetes](https://github.com/Termix-SSH/Plugin-Kubernetes) | Manage Kubernetes clusters from your hosts over SSH, with logs, metrics, YAML and a shell. |
|
|
||||||
| [LDAP](https://github.com/Termix-SSH/Plugin-LDAP) | Sign in with your LDAP or Active Directory username and password. |
|
| [LDAP](https://github.com/Termix-SSH/Plugin-LDAP) | Sign in with your LDAP or Active Directory username and password. |
|
||||||
| [Login Security](https://github.com/Termix-SSH/Plugin-Login-Security) | Sign people out when idle, limit session length, turn off remember me and require a second factor at login. |
|
| [Login Security](https://github.com/Termix-SSH/Plugin-Login-Security) | Sign people out when idle, limit session length, turn off remember me and require a second factor at login. |
|
||||||
| [Network Topology](https://github.com/Termix-SSH/Plugin-Network-Topology) | Draws your hosts and the links between them as a live, interactive graph. |
|
| [Network Topology](https://github.com/Termix-SSH/Plugin-Network-Topology) | Draws your hosts and the links between them as a live, interactive graph. |
|
||||||
@@ -86,11 +83,8 @@ Plugins add everything else. The official ones ship with Termix, and admins can
|
|||||||
| [Passkeys](https://github.com/Termix-SSH/Plugin-WebAuthn) | Sign in with a passkey or security key instead of a password. |
|
| [Passkeys](https://github.com/Termix-SSH/Plugin-WebAuthn) | Sign in with a passkey or security key instead of a password. |
|
||||||
| [Proxmox](https://github.com/Termix-SSH/Plugin-Proxmox) | Import Proxmox VE guests as hosts, keep them in sync and watch node stats. |
|
| [Proxmox](https://github.com/Termix-SSH/Plugin-Proxmox) | Import Proxmox VE guests as hosts, keep them in sync and watch node stats. |
|
||||||
| [Remote Desktop](https://github.com/Termix-SSH/Plugin-Remote-Desktop) | RDP, VNC and Telnet sessions in your browser, with clipboard, file transfer, sharing and recording. |
|
| [Remote Desktop](https://github.com/Termix-SSH/Plugin-Remote-Desktop) | RDP, VNC and Telnet sessions in your browser, with clipboard, file transfer, sharing and recording. |
|
||||||
| [Reverse Proxy](https://github.com/Termix-SSH/Plugin-Reverse-Proxy) | Reverse proxy for TCP, UDP, HTTP and HTTPS with free certificates and Termix login, managed per host. |
|
|
||||||
| [Samba Shares](https://github.com/Termix-SSH/Plugin-Samba) | Create and manage Samba (SMB) file shares, users and permissions on your hosts from one tab. |
|
|
||||||
| [Secret Sources](https://github.com/Termix-SSH/Plugin-Secret-Sources) | Pull host passwords and keys from 1Password when you connect instead of storing them in Termix. |
|
| [Secret Sources](https://github.com/Termix-SSH/Plugin-Secret-Sources) | Pull host passwords and keys from 1Password when you connect instead of storing them in Termix. |
|
||||||
| [Serial](https://github.com/Termix-SSH/Plugin-Serial) | Open a serial console to a router, switch or other device plugged into your computer. |
|
| [Serial](https://github.com/Termix-SSH/Plugin-Serial) | Open a serial console to a router, switch or other device plugged into your computer. |
|
||||||
| [Services](https://github.com/Termix-SSH/Plugin-Service-Manager) | Manage services, timers and logs on your hosts over SSH. |
|
|
||||||
| [Session Recording](https://github.com/Termix-SSH/Plugin-Session-Recording) | Record terminal and remote desktop sessions and play them back or download them later. |
|
| [Session Recording](https://github.com/Termix-SSH/Plugin-Session-Recording) | Record terminal and remote desktop sessions and play them back or download them later. |
|
||||||
| [Session Sharing](https://github.com/Termix-SSH/Plugin-Session-Sharing) | Share live sessions by link or with another user, and present sessions to a group in meeting rooms. |
|
| [Session Sharing](https://github.com/Termix-SSH/Plugin-Session-Sharing) | Share live sessions by link or with another user, and present sessions to a group in meeting rooms. |
|
||||||
| [Single sign-on](https://github.com/Termix-SSH/Plugin-SSO) | Sign in with any OpenID Connect provider, GitHub or Google. |
|
| [Single sign-on](https://github.com/Termix-SSH/Plugin-SSO) | Sign in with any OpenID Connect provider, GitHub or Google. |
|
||||||
|
|||||||
+27
-69
@@ -9,8 +9,8 @@
|
|||||||
{
|
{
|
||||||
"id": "ai",
|
"id": "ai",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-AI/releases/download/v1.2.0/ai-1.2.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-AI/releases/download/v1.1.0/ai-1.1.0.tmxplug",
|
||||||
"sha256": "3b486133af78ae5168e6f5b8a4c71f3db3403cfdae537ed1ca8d6cc16035d8dd"
|
"sha256": "71dda6940d62c9eede7821bbac21e99443fbc08728a47759f215e2416f1d162f"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alerts",
|
"id": "alerts",
|
||||||
@@ -22,43 +22,31 @@
|
|||||||
"consent": false
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "audit-tools",
|
|
||||||
"source": "tmxplug",
|
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Audit-Tools/releases/download/v1.0.0/audit-tools-1.0.0.tmxplug",
|
|
||||||
"sha256": "131479c294d692b1c0ed4d6cec273730a3a67a28ecd44825ffd36bade9aa863b"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "automations",
|
"id": "automations",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Automations/releases/download/v1.0.3/automations-1.0.3.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Automations/releases/download/v1.0.2/automations-1.0.2.tmxplug",
|
||||||
"sha256": "b5e5b39a053010b72ad1ba03ea6bb05afbbb2b789a86e0e98634d9ada543d02d"
|
"sha256": "674eb04276b4a0c77c794b30647f8680218a78de8d52b73e99ec900cc1b3fdb3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "classic-layout",
|
"id": "classic-layout",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Classic-Layout/releases/download/v1.0.1/classic-layout-1.0.1.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Classic-Layout/releases/download/v1.0.0/classic-layout-1.0.0.tmxplug",
|
||||||
"sha256": "6b2d9acd77ef4b33fa0761c1aea7ed3c6171c325ee8febcedff94b5b2bc7be34",
|
"sha256": "3a51cc9a4c966478f2ebfd58dbbe025ff0ebe8bbdcd3ba73156151e555f62dec",
|
||||||
"defaultLook": true
|
"defaultLook": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "classic-themes",
|
"id": "classic-themes",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Classic-Themes/releases/download/v1.0.0/classic-themes-1.0.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Classic-Themes/releases/download/v1.0.0/classic-themes-1.0.0.tmxplug",
|
||||||
"sha256": "919283f95701d41727c826637a7d349bd6a646abd61f9ac9017149bd0bee782c",
|
"sha256": "17561e569811476ea4e1f7cdf8d9afbe33edf061bd09f6b05699b44968da41ad",
|
||||||
"defaultLook": true
|
"defaultLook": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "credential-tools",
|
|
||||||
"source": "tmxplug",
|
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Credential-Tools/releases/download/v1.0.0/credential-tools-1.0.0.tmxplug",
|
|
||||||
"sha256": "413eff710b5c832a2575a890bbab1c90d28ea55f10cd7a7b94b851d51dd77dce"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "docker",
|
"id": "docker",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Docker/releases/download/v1.1.0/docker-1.1.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Docker/releases/download/v1.0.2/docker-1.0.2.tmxplug",
|
||||||
"sha256": "da32dfbe9d4cffec5d80c4a1037a79e7591a3d16c50f882db6a146322a50c2b7",
|
"sha256": "0e1c722682faac6a027fd9b01e6910eb53ce5acb13332c6c8d8868554f3701ab",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": false
|
"consent": false
|
||||||
@@ -67,8 +55,8 @@
|
|||||||
{
|
{
|
||||||
"id": "file-manager",
|
"id": "file-manager",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-File-Manager/releases/download/v1.1.0/file-manager-1.1.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-File-Manager/releases/download/v1.0.3/file-manager-1.0.3.tmxplug",
|
||||||
"sha256": "807e9ffa80d6054a5911522721a555e0daa3e50f6edffdebe97efe7c91aea7a2",
|
"sha256": "250e9779a6b1caf11721f1ee2baf76655ae6e52d963e894d8db84eb9677cf0b3",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": false
|
"consent": false
|
||||||
@@ -83,49 +71,25 @@
|
|||||||
{
|
{
|
||||||
"id": "homepage",
|
"id": "homepage",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Homepage/releases/download/v1.1.0/homepage-1.1.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Homepage/releases/download/v1.0.2/homepage-1.0.2.tmxplug",
|
||||||
"sha256": "5a9d5d2cc337fcee5590ea957e35ddeea5a71596aeb59410bf1420065b9b5b00"
|
"sha256": "88cdea8ba8f458fcf124e7ab97e3d419896d8d22ffa9a372af34f1818872975b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "host-metrics",
|
"id": "host-metrics",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Host-Metrics/releases/download/v1.1.0/host-metrics-1.1.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Host-Metrics/releases/download/v1.0.3/host-metrics-1.0.3.tmxplug",
|
||||||
"sha256": "4f1d422ec1d0565c79f94fa814ee0864d556d7367793b11035e68239e49e297f",
|
"sha256": "8f6022568349fc4318d509781bdb9d5b40973c010a9e27f720eed6baf68b881e",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": false
|
"consent": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "host-status",
|
|
||||||
"source": "tmxplug",
|
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Host-Status/releases/download/v1.0.0/host-status-1.0.0.tmxplug",
|
|
||||||
"sha256": "1ba62577dc7373a2a2787bab7efdb89b544a30a8003b1de91ce51f372fc4f170"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hosts-config",
|
|
||||||
"source": "tmxplug",
|
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Hosts-Config/releases/download/v1.0.0/hosts-config-1.0.0.tmxplug",
|
|
||||||
"sha256": "60a2b2a6d9ba13bad12a511c93af70a30a10fbc560066b6419acf533d806a3ad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "import-export",
|
|
||||||
"source": "tmxplug",
|
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Import-Export/releases/download/v1.0.0/import-export-1.0.0.tmxplug",
|
|
||||||
"sha256": "6670b6c147e81eed0ddfd249f9a7c9564f1ab50e45f2771161d2e30c47f6fd58"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "ldap",
|
"id": "ldap",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-LDAP/releases/download/v1.0.2/ldap-1.0.2.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-LDAP/releases/download/v1.0.2/ldap-1.0.2.tmxplug",
|
||||||
"sha256": "9ef269ad281f1c68cea53e0d43ccb7a5d4f949f11b30b22eac3d01047b20eb03"
|
"sha256": "9ef269ad281f1c68cea53e0d43ccb7a5d4f949f11b30b22eac3d01047b20eb03"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "login-security",
|
|
||||||
"source": "tmxplug",
|
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Login-Security/releases/download/v1.0.0/login-security-1.0.0.tmxplug",
|
|
||||||
"sha256": "b21dff5722e72a02022d563ef27130e8b56a53e273f1668fade4ec7d030374aa"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "network-topology",
|
"id": "network-topology",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
@@ -147,8 +111,8 @@
|
|||||||
{
|
{
|
||||||
"id": "remote-desktop",
|
"id": "remote-desktop",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Remote-Desktop/releases/download/v1.2.0/remote-desktop-1.2.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Remote-Desktop/releases/download/v1.1.0/remote-desktop-1.1.0.tmxplug",
|
||||||
"sha256": "0de40896bfeb4b1a702c3ce815355b7b7b0bb4bfe424f33e905bb5a0209d02f6",
|
"sha256": "6f74f1d26f91a43b2d2203417d744211f366c335d5fff7bed34bfce33874dd01",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": false
|
"consent": false
|
||||||
@@ -195,8 +159,8 @@
|
|||||||
{
|
{
|
||||||
"id": "ssh-terminal",
|
"id": "ssh-terminal",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-SSH-Terminal/releases/download/v1.1.0/ssh-terminal-1.1.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-SSH-Terminal/releases/download/v1.0.3/ssh-terminal-1.0.3.tmxplug",
|
||||||
"sha256": "dacdbadb3c6a0d7014b1de4f62b499f80abf358c7799a6e6326674660b055e4a",
|
"sha256": "bc288bd843e37b64bcda7462d175039147b75dc6de8866769e4a938036a9d023",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": false
|
"consent": false
|
||||||
@@ -219,12 +183,6 @@
|
|||||||
"url": "https://github.com/Termix-SSH/Plugin-Step-CA/releases/download/v1.0.2/step-ca-1.0.2.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Step-CA/releases/download/v1.0.2/step-ca-1.0.2.tmxplug",
|
||||||
"sha256": "685b644fe44c2d3fa007baee16e8ea8a7d5204f039ebc8aa65e8093ad93adf8a"
|
"sha256": "685b644fe44c2d3fa007baee16e8ea8a7d5204f039ebc8aa65e8093ad93adf8a"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "sync",
|
|
||||||
"source": "tmxplug",
|
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Sync/releases/download/v1.0.0/sync-1.0.0.tmxplug",
|
|
||||||
"sha256": "12ced05523f7c1a9151b5e680726bd31f53888e8bb0047d7bd12a2570ea168bf"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "tailscale",
|
"id": "tailscale",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
@@ -250,8 +208,8 @@
|
|||||||
{
|
{
|
||||||
"id": "tmux-monitor",
|
"id": "tmux-monitor",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Tmux-Monitor/releases/download/v1.0.4/tmux-monitor-1.0.4.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Tmux-Monitor/releases/download/v1.0.3/tmux-monitor-1.0.3.tmxplug",
|
||||||
"sha256": "42b8a2284e68a321c1736984f5c641b1857c87c6a65ce8cf694c6bc946de2444"
|
"sha256": "f152d64f433d3f1de7c3fdf172e752ea7e333909f0fdda77336a6b8dddb9f242"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "totp",
|
"id": "totp",
|
||||||
@@ -266,8 +224,8 @@
|
|||||||
{
|
{
|
||||||
"id": "tunnels",
|
"id": "tunnels",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Tunnels/releases/download/v1.1.0/tunnels-1.1.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Tunnels/releases/download/v1.0.3/tunnels-1.0.3.tmxplug",
|
||||||
"sha256": "b1e4f1b6dd2bbd15a9a240ae1c1d381111c3609258c8946d19493a83f861ea1e",
|
"sha256": "d0de9e59ccc9e1a0f2817dd1ec9951021f2d7e89304894bb1e8cefd65aebff0b",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": false
|
"consent": false
|
||||||
@@ -294,14 +252,14 @@
|
|||||||
{
|
{
|
||||||
"id": "web-endpoint",
|
"id": "web-endpoint",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-Web-Endpoint/releases/download/v1.0.3/web-endpoint-1.0.3.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-Web-Endpoint/releases/download/v1.0.2/web-endpoint-1.0.2.tmxplug",
|
||||||
"sha256": "014cbf23c82cab20e485bc4f4c9b8baa779aee9bb3ed644b41f22a9b1be95687"
|
"sha256": "0326edb1052d1debd083004c2dd1711d21a902cfacb55ce74d1a8856a5122477"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "webauthn",
|
"id": "webauthn",
|
||||||
"source": "tmxplug",
|
"source": "tmxplug",
|
||||||
"url": "https://github.com/Termix-SSH/Plugin-WebAuthn/releases/download/v1.1.0/webauthn-1.1.0.tmxplug",
|
"url": "https://github.com/Termix-SSH/Plugin-WebAuthn/releases/download/v1.0.2/webauthn-1.0.2.tmxplug",
|
||||||
"sha256": "83a61a530db0273f9fef2080d6aa027a82ef8374fb4e0f158a8559a2770299de",
|
"sha256": "8269f6e407897332648c8526b65383c4db071ea782bf1e58aa7c7c00627f3f4b",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"consent": false
|
"consent": false
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.5",
|
"version": "26.10.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.5",
|
"version": "26.10.3",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "26.10.5",
|
"version": "26.10.3",
|
||||||
"description": "Self-hosted, plugin-based server management.",
|
"description": "Self-hosted, plugin-based server management.",
|
||||||
"author": "Karmaa",
|
"author": "Karmaa",
|
||||||
"main": "electron/main.cjs",
|
"main": "electron/main.cjs",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-node-pty.cjs && node scripts/patch-ssh2-agent.cjs && node scripts/patch-ssh2-sk.cjs",
|
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-node-pty.cjs && node scripts/patch-ssh2-agent.cjs && node scripts/patch-ssh2-sk.cjs",
|
||||||
"prepare": "husky || true",
|
"prepare": "husky || true",
|
||||||
"prebuild": "node scripts/write-electron-build-info.cjs",
|
"prebuild": "node scripts/write-electron-build-info.cjs",
|
||||||
"lint": "node scripts/generate-dialect-schema.cjs --check && eslint . && node scripts/check-importmap-csp.cjs && node scripts/check-core-plugin-ids.cjs && node scripts/check-sdk-ui-exports.cjs && node scripts/check-button-colors.cjs && node scripts/check-sdk-guide.cjs && node scripts/check-test-locations.cjs && node scripts/check-changelog.cjs && node scripts/check-env-catalog.cjs",
|
"lint": "node scripts/generate-dialect-schema.cjs --check && eslint . && node scripts/check-importmap-csp.cjs && node scripts/check-core-plugin-ids.cjs && node scripts/check-sdk-ui-exports.cjs && node scripts/check-sdk-guide.cjs && node scripts/check-test-locations.cjs && node scripts/check-changelog.cjs && node scripts/check-env-catalog.cjs",
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "eslint --fix .",
|
||||||
"type-check": "npm run build:sdk && tsc -b --force",
|
"type-check": "npm run build:sdk && tsc -b --force",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 1.0.20
|
|
||||||
|
|
||||||
Plugin API 1.12.0.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `columns` and `onColumns` on `ViewToggle` show a cards per row picker: Auto fits as many as the pane has room for, or pick 1 to 6
|
|
||||||
- `size` and `minCardWidth` on `DataView`, so a card grid follows that pick, and the `GridSize` type
|
|
||||||
- `usePluginPanelView` also keeps `columns`, the cards per row pick, with `setColumns`
|
|
||||||
|
|
||||||
## 1.0.19
|
## 1.0.19
|
||||||
|
|
||||||
Plugin API 1.11.0.
|
Plugin API 1.11.0.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Termix plugin guide
|
# Termix plugin guide
|
||||||
|
|
||||||
SDK 1.0.20 | Plugin API 1.12.0
|
SDK 1.0.19 | Plugin API 1.11.0
|
||||||
|
|
||||||
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
||||||
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
||||||
@@ -643,9 +643,6 @@ exports with props is `ui.api.txt` in the Termix repo
|
|||||||
`PanePrompt` for small prompts.
|
`PanePrompt` for small prompts.
|
||||||
- Every tab or panel sits in `PanelShell`. Lists use `PanelList` and `ListRow`, add with `AddButton`,
|
- Every tab or panel sits in `PanelShell`. Lists use `PanelList` and `ListRow`, add with `AddButton`,
|
||||||
forms end with `FormFooter`, empty lists show `EmptyState`.
|
forms end with `FormFooter`, empty lists show `EmptyState`.
|
||||||
- Card grids use `DataView` with `ViewToggle`. Pass `columns` and `onColumns` to `ViewToggle` and the same
|
|
||||||
value as `size` to `DataView`, so users can pick Auto (as many as fit) or a fixed number of cards per
|
|
||||||
row. `usePluginPanelView` keeps the choice as `columns`. Older Termix versions ignore both props.
|
|
||||||
- Forms: `TextField`, `NumberField`, `SelectField`, `SwitchRow`, `SecretField`, `SettingRow` and friends.
|
- Forms: `TextField`, `NumberField`, `SelectField`, `SwitchRow`, `SecretField`, `SettingRow` and friends.
|
||||||
- Host sessions use `ConnectionGate` for the whole connect flow (host keys, TOTP, passphrases, errors).
|
- Host sessions use `ConnectionGate` for the whole connect flow (host keys, TOTP, passphrases, errors).
|
||||||
- Mobile: check `useIsMobile()` and give small screens a layout that works. The desktop app always uses the
|
- Mobile: check `useIsMobile()` and give small screens a layout that works. The desktop app always uses the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"name": "@termix-ssh/plugin-sdk",
|
||||||
"version": "1.0.20",
|
"version": "1.0.19",
|
||||||
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1711,49 +1711,28 @@ export function usePluginUiPreferences<
|
|||||||
return bridge.usePluginUiPreferences(bridge.usePluginId()) as never;
|
return bridge.usePluginUiPreferences(bridge.usePluginId()) as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanelGridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
|
||||||
const PANEL_GRID_SIZES: readonly string[] = [
|
|
||||||
"auto",
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5",
|
|
||||||
"6",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid or list, row density and cards per row for one of this plugin's
|
* Grid or list and row density for one of this plugin's views, kept in its
|
||||||
* views, kept in its Appearance area as `<prefix>viewMode`, `<prefix>density`
|
* Appearance area as `<prefix>viewMode` and `<prefix>density`. Declare those
|
||||||
* and `<prefix>columns`. Declare those keys in contributes.uiPresets so each
|
* keys in contributes.uiPresets so each preset level can pick a default.
|
||||||
* preset level can pick a default. Hand columns to ViewToggle and DataView.
|
|
||||||
*/
|
*/
|
||||||
export function usePluginPanelView(prefix = ""): {
|
export function usePluginPanelView(prefix = ""): {
|
||||||
view: "grid" | "list";
|
view: "grid" | "list";
|
||||||
density: "comfortable" | "compact";
|
density: "comfortable" | "compact";
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
columns: PanelGridSize;
|
|
||||||
setView: (view: "grid" | "list") => void;
|
setView: (view: "grid" | "list") => void;
|
||||||
setDensity: (density: "comfortable" | "compact") => void;
|
setDensity: (density: "comfortable" | "compact") => void;
|
||||||
setColumns: (columns: PanelGridSize) => void;
|
|
||||||
} {
|
} {
|
||||||
const { values, set } = usePluginUiPreferences();
|
const { values, set } = usePluginUiPreferences();
|
||||||
const view = values[`${prefix}viewMode`] === "list" ? "list" : "grid";
|
const view = values[`${prefix}viewMode`] === "list" ? "list" : "grid";
|
||||||
const density =
|
const density =
|
||||||
values[`${prefix}density`] === "compact" ? "compact" : "comfortable";
|
values[`${prefix}density`] === "compact" ? "compact" : "comfortable";
|
||||||
const saved = values[`${prefix}columns`];
|
|
||||||
const columns =
|
|
||||||
typeof saved === "string" && PANEL_GRID_SIZES.includes(saved)
|
|
||||||
? (saved as PanelGridSize)
|
|
||||||
: "auto";
|
|
||||||
return {
|
return {
|
||||||
view,
|
view,
|
||||||
density,
|
density,
|
||||||
compact: density === "compact",
|
compact: density === "compact",
|
||||||
columns,
|
|
||||||
setView: (next) => set(`${prefix}viewMode`, next),
|
setView: (next) => set(`${prefix}viewMode`, next),
|
||||||
setDensity: (next) => set(`${prefix}density`, next),
|
setDensity: (next) => set(`${prefix}density`, next),
|
||||||
setColumns: (next) => set(`${prefix}columns`, next),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export {
|
|||||||
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
||||||
* version by the server.
|
* version by the server.
|
||||||
*/
|
*/
|
||||||
export const PLUGIN_API_VERSION = "1.12.0";
|
export const PLUGIN_API_VERSION = "1.11.0";
|
||||||
|
|
||||||
/** The API major, the default engine.api for a new plugin. */
|
/** The API major, the default engine.api for a new plugin. */
|
||||||
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ DEFAULT_FOLDER_COLOR (value): "#f59145"
|
|||||||
DEFAULT_FOLDER_ICON (value): "folder"
|
DEFAULT_FOLDER_ICON (value): "folder"
|
||||||
DashboardCardId (type): DashboardCardId
|
DashboardCardId (type): DashboardCardId
|
||||||
DataColumn (type): DataColumn<T>
|
DataColumn (type): DataColumn<T>
|
||||||
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, size, minCardWidth, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; size?: GridSize; minCardWidth?: number; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
||||||
DefaultOverrides (type): DefaultOverrides
|
DefaultOverrides (type): DefaultOverrides
|
||||||
DefaultsOnly (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
DefaultsOnly (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
||||||
DesktopUpdateMode (type): DesktopUpdateMode
|
DesktopUpdateMode (type): DesktopUpdateMode
|
||||||
@@ -131,7 +131,6 @@ GridCardCatalogEntry (type): GridCardCatalogEntry
|
|||||||
GridColSpan (type): GridColSpan
|
GridColSpan (type): GridColSpan
|
||||||
GridColumns (type): GridColumns
|
GridColumns (type): GridColumns
|
||||||
GridLayout (type): GridLayout
|
GridLayout (type): GridLayout
|
||||||
GridSize (type): GridSize
|
|
||||||
GridSlot (type): GridSlot
|
GridSlot (type): GridSlot
|
||||||
GroupHeading (value): ({ title, count, action, className, }: { title: string; count?: number; action?: ReactNode; className?: string; }) => Element
|
GroupHeading (value): ({ title, count, action, className, }: { title: string; count?: number; action?: ReactNode; className?: string; }) => Element
|
||||||
HexColorPicker (value): ({ value, onChange, }: { value: string; onChange: (hex: string) => void; }) => Element
|
HexColorPicker (value): ({ value, onChange, }: { value: string; onChange: (hex: string) => void; }) => Element
|
||||||
@@ -322,7 +321,7 @@ UserRole (type): UserRole
|
|||||||
UserSecondFactorSummary (type): UserSecondFactorSummary
|
UserSecondFactorSummary (type): UserSecondFactorSummary
|
||||||
VersionBadge (value): ({ status, releaseUrl, className, }: VersionBadgeProps) => Element
|
VersionBadge (value): ({ status, releaseUrl, className, }: VersionBadgeProps) => Element
|
||||||
VersionInfo (type): VersionInfo
|
VersionInfo (type): VersionInfo
|
||||||
ViewToggle (value): ({ view, onView, density, onDensity, columns, onColumns, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; columns?: GridSize; onColumns?: (next: GridSize) => void; }) => Element
|
ViewToggle (value): ({ view, onView, density, onDensity, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; }) => Element
|
||||||
WebSocketConnectionTarget (type): WebSocketConnectionTarget
|
WebSocketConnectionTarget (type): WebSocketConnectionTarget
|
||||||
WidgetTitle (value): ({ title, icon, fallback }: WidgetTitleProps) => Element
|
WidgetTitle (value): ({ title, icon, fallback }: WidgetTitleProps) => Element
|
||||||
actionOrder (value): (action: HostActionDef) => number
|
actionOrder (value): (action: HostActionDef) => number
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* Buttons are never white.
|
|
||||||
*
|
|
||||||
* Every Button variant is the brand orange with a different background
|
|
||||||
* opacity (or red for destructive, or transparent for ghost). Fails when a
|
|
||||||
* variant in src/ui/components/button.tsx uses a neutral fill, or when a
|
|
||||||
* <Button> anywhere in core or the local plugin checkouts sets a neutral fill
|
|
||||||
* in its className.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require("node:fs");
|
|
||||||
const path = require("node:path");
|
|
||||||
|
|
||||||
const NEUTRAL =
|
|
||||||
/(?:^|[\s"'`])(?:dark:)?(?:bg-(?:primary|white|foreground|background|secondary|card|popover|muted|accent|input|neutral-\d+|zinc-\d+|gray-\d+|slate-\d+|stone-\d+)(?![\w-])|text-primary-foreground)/;
|
|
||||||
|
|
||||||
/** The opening tag text of each <Button ...> in a source file. */
|
|
||||||
function buttonTags(source) {
|
|
||||||
const tags = [];
|
|
||||||
const re = /<Button(?=[\s/>])/g;
|
|
||||||
let match;
|
|
||||||
while ((match = re.exec(source))) {
|
|
||||||
let depth = 0;
|
|
||||||
let quote = "";
|
|
||||||
let i = match.index + match[0].length;
|
|
||||||
for (; i < source.length; i++) {
|
|
||||||
const c = source[i];
|
|
||||||
if (quote) {
|
|
||||||
if (c === "\\") i++;
|
|
||||||
else if (c === quote) quote = "";
|
|
||||||
} else if (c === '"' || c === "'" || c === "`") quote = c;
|
|
||||||
else if (c === "{") depth++;
|
|
||||||
else if (c === "}") depth--;
|
|
||||||
else if (c === ">" && depth === 0) break;
|
|
||||||
}
|
|
||||||
tags.push(source.slice(match.index, i + 1));
|
|
||||||
}
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Variant strings from the cva block of button.tsx. */
|
|
||||||
function variantLines(source) {
|
|
||||||
const start = source.indexOf("variant: {");
|
|
||||||
const end = source.indexOf("size: {");
|
|
||||||
if (start < 0 || end < 0) return [];
|
|
||||||
return source
|
|
||||||
.slice(start, end)
|
|
||||||
.split("\n")
|
|
||||||
.filter((line) => /["`]/.test(line) && !line.includes("variant: {"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function walk(dir, out) {
|
|
||||||
if (!fs.existsSync(dir)) return;
|
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
if (["node_modules", "dist", "tests", "__fixtures__"].includes(entry.name))
|
|
||||||
continue;
|
|
||||||
const full = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) walk(full, out);
|
|
||||||
else if (entry.name.endsWith(".tsx")) out.push(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Problems found under the given roots. */
|
|
||||||
function scan(roots, buttonFile) {
|
|
||||||
const problems = [];
|
|
||||||
if (buttonFile && fs.existsSync(buttonFile)) {
|
|
||||||
for (const line of variantLines(fs.readFileSync(buttonFile, "utf8"))) {
|
|
||||||
if (NEUTRAL.test(line)) {
|
|
||||||
problems.push(
|
|
||||||
`${buttonFile}: neutral fill in a variant: ${line.trim()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const files = [];
|
|
||||||
for (const root of roots) walk(root, files);
|
|
||||||
for (const file of files) {
|
|
||||||
for (const tag of buttonTags(fs.readFileSync(file, "utf8"))) {
|
|
||||||
if (NEUTRAL.test(tag)) {
|
|
||||||
problems.push(
|
|
||||||
`${file}: <Button> with a neutral fill: ${tag.slice(0, 120)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return problems;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { scan, buttonTags, variantLines };
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
const core = path.resolve(__dirname, "..");
|
|
||||||
const plugins = path.resolve(core, "..", "Plugins");
|
|
||||||
const roots = [path.join(core, "src", "ui")];
|
|
||||||
if (fs.existsSync(plugins)) {
|
|
||||||
for (const entry of fs.readdirSync(plugins)) {
|
|
||||||
roots.push(path.join(plugins, entry, "src"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const problems = scan(
|
|
||||||
roots,
|
|
||||||
path.join(core, "src", "ui", "components", "button.tsx"),
|
|
||||||
);
|
|
||||||
if (problems.length) {
|
|
||||||
console.error("Buttons must be the brand orange, never white or neutral:");
|
|
||||||
for (const p of problems) console.error(" " + p);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log("ok button colors");
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it } from "vitest";
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const { scan, buttonTags } = require("./check-button-colors.cjs") as {
|
|
||||||
scan: (roots: string[], buttonFile?: string) => string[];
|
|
||||||
buttonTags: (source: string) => string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
let root: string | null = null;
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
||||||
root = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
function fixture(files: Record<string, string>): string {
|
|
||||||
root = fs.mkdtempSync(path.join(os.tmpdir(), "button-colors-"));
|
|
||||||
for (const [file, content] of Object.entries(files)) {
|
|
||||||
const full = path.join(root, file);
|
|
||||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
||||||
fs.writeFileSync(full, content);
|
|
||||||
}
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("buttonTags", () => {
|
|
||||||
it("reads a tag with arrow functions inside", () => {
|
|
||||||
const tags = buttonTags(
|
|
||||||
`<Button onClick={() => go(a > b)} className="x">Go</Button>`,
|
|
||||||
);
|
|
||||||
expect(tags).toHaveLength(1);
|
|
||||||
expect(tags[0]).toContain('className="x"');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("check-button-colors", () => {
|
|
||||||
it("accepts orange buttons and hover fills", () => {
|
|
||||||
const dir = fixture({
|
|
||||||
"a.tsx": `<Button className="bg-accent-brand/15 text-accent-brand hover:bg-muted" />`,
|
|
||||||
});
|
|
||||||
expect(scan([dir])).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects a white or neutral fill on a Button", () => {
|
|
||||||
for (const cls of [
|
|
||||||
"bg-primary",
|
|
||||||
"bg-white",
|
|
||||||
"bg-foreground text-background",
|
|
||||||
"dark:bg-secondary",
|
|
||||||
"bg-muted",
|
|
||||||
"text-primary-foreground",
|
|
||||||
]) {
|
|
||||||
const dir = fixture({ "a.tsx": `<Button className="${cls}">x</Button>` });
|
|
||||||
expect(scan([dir]), cls).toHaveLength(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects a neutral fill in a variant", () => {
|
|
||||||
const dir = fixture({
|
|
||||||
"button.tsx": `variant: {\n default: "bg-primary text-primary-foreground",\n ghost: "hover:bg-muted",\n},\nsize: {`,
|
|
||||||
});
|
|
||||||
expect(scan([], path.join(dir, "button.tsx"))).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("passes on the real button", () => {
|
|
||||||
const button = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"src",
|
|
||||||
"ui",
|
|
||||||
"components",
|
|
||||||
"button.tsx",
|
|
||||||
);
|
|
||||||
expect(scan([], button)).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -151,7 +151,6 @@ const ALLOWED = [
|
|||||||
"GridCardCatalogEntry",
|
"GridCardCatalogEntry",
|
||||||
"GridColSpan",
|
"GridColSpan",
|
||||||
"GridColumns",
|
"GridColumns",
|
||||||
"GridSize",
|
|
||||||
"GridLayout",
|
"GridLayout",
|
||||||
"GridSlot",
|
"GridSlot",
|
||||||
"GroupHeading",
|
"GroupHeading",
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ const packages = [
|
|||||||
"@img/sharp-win32-x64",
|
"@img/sharp-win32-x64",
|
||||||
].map((name) => `${name}@${sharpPackage.optionalDependencies[name]}`);
|
].map((name) => `${name}@${sharpPackage.optionalDependencies[name]}`);
|
||||||
|
|
||||||
// A .cmd file only starts through a shell on current Node.
|
|
||||||
execFileSync("npm.cmd", ["install", "--force", "--no-save", ...packages], {
|
execFileSync("npm.cmd", ["install", "--force", "--no-save", ...packages], {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
shell: true,
|
|
||||||
});
|
});
|
||||||
@@ -137,19 +137,9 @@ function verifyArtifact(artifact) {
|
|||||||
console.log(`Verified macOS sharp packaging: ${path.basename(artifact)}`);
|
console.log(`Verified macOS sharp packaging: ${path.basename(artifact)}`);
|
||||||
} finally {
|
} finally {
|
||||||
if (mountedAt) {
|
if (mountedAt) {
|
||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
spawnSync("hdiutil", ["detach", mountedAt], { stdio: "ignore" });
|
||||||
const args =
|
|
||||||
attempt < 3 ? ["detach", mountedAt] : ["detach", "-force", mountedAt];
|
|
||||||
if (spawnSync("hdiutil", args, { stdio: "ignore" }).status === 0) break;
|
|
||||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fs.rmSync(temporaryRoot, {
|
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
maxRetries: 5,
|
|
||||||
retryDelay: 1000,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ import type { PluginTableDefinition } from "@termix-ssh/plugin-sdk/db";
|
|||||||
import * as syncRegistry from "./sync-registry.js";
|
import * as syncRegistry from "./sync-registry.js";
|
||||||
import { provideSync } from "../sync/provider.js";
|
import { provideSync } from "../sync/provider.js";
|
||||||
import { createSyncHost } from "../sync/host.js";
|
import { createSyncHost } from "../sync/host.js";
|
||||||
import type {
|
import type { PluginSyncHost } from "@termix-ssh/plugin-sdk/backend";
|
||||||
PluginSync,
|
|
||||||
PluginSyncHost,
|
|
||||||
} from "@termix-ssh/plugin-sdk/backend";
|
|
||||||
import {
|
import {
|
||||||
needsExplicitPersist,
|
needsExplicitPersist,
|
||||||
resolveDatabaseDialect,
|
resolveDatabaseDialect,
|
||||||
@@ -196,7 +193,7 @@ export function createPluginContext(
|
|||||||
): PluginContext {
|
): PluginContext {
|
||||||
const pluginId = manifest.id;
|
const pluginId = manifest.id;
|
||||||
let syncHost: PluginSyncHost | undefined;
|
let syncHost: PluginSyncHost | undefined;
|
||||||
const syncApi: PluginSync = {
|
const syncApi: PluginContext["sync"] = {
|
||||||
registerEntity: (entity) => {
|
registerEntity: (entity) => {
|
||||||
const dispose = syncRegistry.registerEntity(pluginId, entity);
|
const dispose = syncRegistry.registerEntity(pluginId, entity);
|
||||||
handle.bag.add(dispose, `sync entity ${entity.type}`);
|
handle.bag.add(dispose, `sync entity ${entity.type}`);
|
||||||
@@ -212,7 +209,7 @@ export function createPluginContext(
|
|||||||
remove();
|
remove();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
} as PluginSync;
|
} as PluginContext["sync"];
|
||||||
// Not enumerable, so tools that walk the context do not trip the check.
|
// Not enumerable, so tools that walk the context do not trip the check.
|
||||||
Object.defineProperty(syncApi, "host", {
|
Object.defineProperty(syncApi, "host", {
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
|
|||||||
@@ -10,17 +10,16 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
"border-accent-brand/40 bg-accent-brand/15 text-accent-brand hover:bg-accent-brand/25 aria-expanded:bg-accent-brand/25",
|
|
||||||
outline:
|
outline:
|
||||||
"border-accent-brand/40 bg-transparent text-accent-brand hover:bg-accent-brand/10 aria-expanded:bg-accent-brand/10",
|
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
secondary:
|
secondary:
|
||||||
"border-accent-brand/20 bg-accent-brand/10 text-accent-brand hover:bg-accent-brand/20 aria-expanded:bg-accent-brand/20",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
link: "text-accent-brand underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import type React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
PANEL,
|
PANEL,
|
||||||
gridSizeStyle,
|
|
||||||
type GridSize,
|
|
||||||
type PanelDensity,
|
type PanelDensity,
|
||||||
type PanelViewMode,
|
type PanelViewMode,
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
@@ -69,8 +67,6 @@ export function DataView<T>({
|
|||||||
getKey,
|
getKey,
|
||||||
renderCard,
|
renderCard,
|
||||||
columns,
|
columns,
|
||||||
size,
|
|
||||||
minCardWidth,
|
|
||||||
listColumns,
|
listColumns,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
isRowActive,
|
isRowActive,
|
||||||
@@ -82,12 +78,7 @@ export function DataView<T>({
|
|||||||
density?: PanelDensity;
|
density?: PanelDensity;
|
||||||
getKey: (item: T) => string;
|
getKey: (item: T) => string;
|
||||||
renderCard: (item: T) => React.ReactNode;
|
renderCard: (item: T) => React.ReactNode;
|
||||||
/** Breakpoint columns, used when no size is given. */
|
|
||||||
columns: GridColumns;
|
columns: GridColumns;
|
||||||
/** Columns the user picked with ViewToggle. Wins over columns. */
|
|
||||||
size?: GridSize;
|
|
||||||
/** Narrowest card in auto mode, in px. */
|
|
||||||
minCardWidth?: number;
|
|
||||||
listColumns: DataColumn<T>[];
|
listColumns: DataColumn<T>[];
|
||||||
onRowClick?: (item: T) => void;
|
onRowClick?: (item: T) => void;
|
||||||
isRowActive?: (item: T) => boolean;
|
isRowActive?: (item: T) => boolean;
|
||||||
@@ -102,19 +93,9 @@ export function DataView<T>({
|
|||||||
|
|
||||||
if (view === "grid") {
|
if (view === "grid") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn("grid", PANEL.gap, gridClass(columns), className)}>
|
||||||
className={cn(
|
|
||||||
"grid",
|
|
||||||
PANEL.gap,
|
|
||||||
!size && gridClass(columns),
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
style={size ? gridSizeStyle(size, minCardWidth) : undefined}
|
|
||||||
>
|
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={getKey(item)} className="flex flex-col *:flex-1">
|
<div key={getKey(item)}>{renderCard(item)}</div>
|
||||||
{renderCard(item)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
import { Children, Fragment, isValidElement, useRef } from "react";
|
import { Children, Fragment, isValidElement, useRef } from "react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import { ArrowLeft, Grid3X3, List, Rows3, Search, X } from "lucide-react";
|
||||||
ArrowLeft,
|
|
||||||
Columns3,
|
|
||||||
Grid3X3,
|
|
||||||
List,
|
|
||||||
Rows3,
|
|
||||||
Search,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/separator";
|
import { Separator } from "@/components/separator";
|
||||||
import { DocsLink } from "@/components/docs-link";
|
import { DocsLink } from "@/components/docs-link";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/select";
|
|
||||||
|
|
||||||
/** One spacing rhythm for every panel and tab. */
|
/** One spacing rhythm for every panel and tab. */
|
||||||
export const PANEL = {
|
export const PANEL = {
|
||||||
@@ -34,37 +19,6 @@ export const PANEL = {
|
|||||||
export type PanelViewMode = "grid" | "list";
|
export type PanelViewMode = "grid" | "list";
|
||||||
export type PanelDensity = "comfortable" | "compact";
|
export type PanelDensity = "comfortable" | "compact";
|
||||||
|
|
||||||
/** Cards per row. "auto" fits as many as the pane has room for. */
|
|
||||||
export type GridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
|
||||||
|
|
||||||
export const GRID_SIZES: readonly GridSize[] = [
|
|
||||||
"auto",
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"3",
|
|
||||||
"4",
|
|
||||||
"5",
|
|
||||||
"6",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sized off the pane's own width, not the window. Cards keep a min width,
|
|
||||||
// so narrow panes drop columns on their own.
|
|
||||||
export function gridSizeStyle(
|
|
||||||
size: GridSize,
|
|
||||||
minCardWidth = 320,
|
|
||||||
gap = "0.5rem",
|
|
||||||
): React.CSSProperties {
|
|
||||||
if (size === "auto") {
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${minCardWidth}px), 1fr))`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const n = Number(size);
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: `repeat(auto-fill, minmax(max(min(100%, 220px), calc((100% - ${n - 1} * ${gap}) / ${n} - 0.5px)), 1fr))`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The header, toolbar band and body every tab and panel draws. */
|
/** The header, toolbar band and body every tab and panel draws. */
|
||||||
export function PanelShell({
|
export function PanelShell({
|
||||||
chrome = true,
|
chrome = true,
|
||||||
@@ -354,25 +308,20 @@ export function Segmented<T extends string>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Grid or list, plus the compact rows toggle and the cards per row picker. */
|
/** Grid or list, plus the compact rows toggle. */
|
||||||
export function ViewToggle({
|
export function ViewToggle({
|
||||||
view,
|
view,
|
||||||
onView,
|
onView,
|
||||||
density,
|
density,
|
||||||
onDensity,
|
onDensity,
|
||||||
columns,
|
|
||||||
onColumns,
|
|
||||||
}: {
|
}: {
|
||||||
view: PanelViewMode;
|
view: PanelViewMode;
|
||||||
onView: (next: PanelViewMode) => void;
|
onView: (next: PanelViewMode) => void;
|
||||||
density?: PanelDensity;
|
density?: PanelDensity;
|
||||||
onDensity?: (next: PanelDensity) => void;
|
onDensity?: (next: PanelDensity) => void;
|
||||||
/** Cards per row in grid view. Pass with onColumns to show the picker. */
|
|
||||||
columns?: GridSize;
|
|
||||||
onColumns?: (next: GridSize) => void;
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toggle = (
|
return (
|
||||||
<div className="flex items-center border border-border">
|
<div className="flex items-center border border-border">
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
active={view === "grid"}
|
active={view === "grid"}
|
||||||
@@ -407,34 +356,6 @@ export function ViewToggle({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!columns || !onColumns || view !== "grid") return toggle;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Select value={columns} onValueChange={(v) => onColumns(v as GridSize)}>
|
|
||||||
<SelectTrigger
|
|
||||||
size="sm"
|
|
||||||
className="w-auto gap-1.5 text-xs"
|
|
||||||
title={t("panel.columns.title")}
|
|
||||||
aria-label={t("panel.columns.title")}
|
|
||||||
>
|
|
||||||
<Columns3 className="size-3.5 text-muted-foreground" />
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{GRID_SIZES.map((size) => (
|
|
||||||
<SelectItem key={size} value={size} className="text-xs">
|
|
||||||
{size === "auto"
|
|
||||||
? t("panel.columns.auto")
|
|
||||||
: t("panel.columns.count", { count: Number(size) })}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{toggle}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleButton({
|
function ToggleButton({
|
||||||
|
|||||||
@@ -1098,8 +1098,6 @@
|
|||||||
"resetEveryone": "Reset the default for everyone",
|
"resetEveryone": "Reset the default for everyone",
|
||||||
"update": "Update {{name}} to {{version}}",
|
"update": "Update {{name}} to {{version}}",
|
||||||
"disable": "Turn off {{name}}",
|
"disable": "Turn off {{name}}",
|
||||||
"enable": "Turn on {{name}}",
|
|
||||||
"startAgain": "Start {{name}} again",
|
|
||||||
"install": "Find layouts and themes",
|
"install": "Find layouts and themes",
|
||||||
"installTitle": "Layouts and themes",
|
"installTitle": "Layouts and themes",
|
||||||
"installButton": "Install",
|
"installButton": "Install",
|
||||||
@@ -1325,6 +1323,11 @@
|
|||||||
"updated": "Recently updated",
|
"updated": "Recently updated",
|
||||||
"category": "Category"
|
"category": "Category"
|
||||||
},
|
},
|
||||||
|
"columns": {
|
||||||
|
"title": "Columns",
|
||||||
|
"auto": "Auto",
|
||||||
|
"count": "{{count}} wide"
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
@@ -1649,12 +1652,7 @@
|
|||||||
"gridView": "Grid",
|
"gridView": "Grid",
|
||||||
"listView": "List",
|
"listView": "List",
|
||||||
"compactRows": "Compact rows",
|
"compactRows": "Compact rows",
|
||||||
"comfortableRows": "Comfortable rows",
|
"comfortableRows": "Comfortable rows"
|
||||||
"columns": {
|
|
||||||
"title": "Columns",
|
|
||||||
"auto": "Auto",
|
|
||||||
"count": "{{count}} wide"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"secretSaved": "Saved. Type to replace it."
|
"secretSaved": "Saved. Type to replace it."
|
||||||
|
|||||||
@@ -1050,7 +1050,6 @@
|
|||||||
"interfaceFontLabel": "Fon Antara Muka",
|
"interfaceFontLabel": "Fon Antara Muka",
|
||||||
"interfaceFontDescription": "Menukar fon antara muka aplikasi. Profil terminal mengekalkan tetapan fon mereka sendiri.",
|
"interfaceFontDescription": "Menukar fon antara muka aplikasi. Profil terminal mengekalkan tetapan fon mereka sendiri.",
|
||||||
"accentColorLabel": "Warna Aksen",
|
"accentColorLabel": "Warna Aksen",
|
||||||
"apiKeyCount_one": "{{count}} kunci",
|
|
||||||
"colorPickerTooltip": "Buka pemilih warna",
|
"colorPickerTooltip": "Buka pemilih warna",
|
||||||
"themeSystem": "Sistem",
|
"themeSystem": "Sistem",
|
||||||
"themeLight": "Terang",
|
"themeLight": "Terang",
|
||||||
|
|||||||
@@ -1003,7 +1003,6 @@
|
|||||||
"hostCount_other": "{{count}} máy chủ"
|
"hostCount_other": "{{count}} máy chủ"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"usersCount_one": "{{count}} người dùng",
|
|
||||||
"rolesCount_one": "vai trò {{count}}",
|
"rolesCount_one": "vai trò {{count}}",
|
||||||
"apiKeysCount_one": "{{count}} khóa",
|
"apiKeysCount_one": "{{count}} khóa",
|
||||||
"sessionsActive_one": "còn {{count}} phiên đang hoạt động",
|
"sessionsActive_one": "còn {{count}} phiên đang hoạt động",
|
||||||
|
|||||||
@@ -944,7 +944,6 @@
|
|||||||
"dashboardTab.stable": "7ce3adafad3e",
|
"dashboardTab.stable": "7ce3adafad3e",
|
||||||
"dashboardTab.onlineCount": "95d81e3c41e5",
|
"dashboardTab.onlineCount": "95d81e3c41e5",
|
||||||
"dashboardTab.hostCount": "4279c8d92ede",
|
"dashboardTab.hostCount": "4279c8d92ede",
|
||||||
"admin.usersCount_one": "e085f3c4a7e3",
|
|
||||||
"admin.rolesCount_one": "78d7eff0d4b9",
|
"admin.rolesCount_one": "78d7eff0d4b9",
|
||||||
"admin.apiKeysCount_one": "88a9751309dc",
|
"admin.apiKeysCount_one": "88a9751309dc",
|
||||||
"admin.sessionsActive_one": "8eaf4563b05d",
|
"admin.sessionsActive_one": "8eaf4563b05d",
|
||||||
@@ -979,7 +978,6 @@
|
|||||||
"newUi.sidebar.userProfile.interfaceFontLabel": "7f587ba745fd",
|
"newUi.sidebar.userProfile.interfaceFontLabel": "7f587ba745fd",
|
||||||
"newUi.sidebar.userProfile.interfaceFontDescription": "8bfbcb4bfa23",
|
"newUi.sidebar.userProfile.interfaceFontDescription": "8bfbcb4bfa23",
|
||||||
"newUi.sidebar.userProfile.accentColorLabel": "2bef525f9fb9",
|
"newUi.sidebar.userProfile.accentColorLabel": "2bef525f9fb9",
|
||||||
"newUi.sidebar.userProfile.apiKeyCount_one": "88a9751309dc",
|
|
||||||
"newUi.sidebar.userProfile.colorPickerTooltip": "402f95aeba34",
|
"newUi.sidebar.userProfile.colorPickerTooltip": "402f95aeba34",
|
||||||
"newUi.sidebar.userProfile.themeSystem": "bc0792d8dc81",
|
"newUi.sidebar.userProfile.themeSystem": "bc0792d8dc81",
|
||||||
"newUi.sidebar.userProfile.themeLight": "a36ef8aba229",
|
"newUi.sidebar.userProfile.themeLight": "a36ef8aba229",
|
||||||
|
|||||||
@@ -81,18 +81,6 @@ function AdminTools({
|
|||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{entry?.installed && !entry.enabled && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => void manager.toggle(entry).then(onRetry)}
|
|
||||||
disabled={manager.busy.has(entry.id)}
|
|
||||||
>
|
|
||||||
<Power />
|
|
||||||
{status.reason === "disabled"
|
|
||||||
? t("looks.repair.enable", { name: entry.name })
|
|
||||||
: t("looks.repair.startAgain", { name: entry.name })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{entry?.installed && entry.enabled && !entry.managedByServer && (
|
{entry?.installed && entry.enabled && !entry.managedByServer && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -79,15 +79,13 @@ export function layoutStatus(input: LayoutStatusInput): LayoutStatus {
|
|||||||
const owner = input.owner;
|
const owner = input.owner;
|
||||||
if (!owner) return repair("missing");
|
if (!owner) return repair("missing");
|
||||||
const summary = owner.summary;
|
const summary = owner.summary;
|
||||||
// Before the enabled check: the server reports a plugin that failed to
|
if (!summary.enabled) return repair("disabled");
|
||||||
// start as not enabled, which hid the real error behind "turned off".
|
|
||||||
if (owner.frontend === "blocked" || summary.state === "blocked") {
|
if (owner.frontend === "blocked" || summary.state === "blocked") {
|
||||||
return repair("blocked", { error: owner.error ?? summary.lastError });
|
return repair("blocked", { error: owner.error ?? summary.lastError });
|
||||||
}
|
}
|
||||||
if (owner.frontend === "failed" || summary.state === "failed") {
|
if (owner.frontend === "failed" || summary.state === "failed") {
|
||||||
return repair("failed", { error: owner.error ?? summary.lastError });
|
return repair("failed", { error: owner.error ?? summary.lastError });
|
||||||
}
|
}
|
||||||
if (!summary.enabled) return repair("disabled");
|
|
||||||
if (owner.frontend === "loading") return { kind: "loading" };
|
if (owner.frontend === "loading") return { kind: "loading" };
|
||||||
// Just chosen: the runtime is about to load its code.
|
// Just chosen: the runtime is about to load its code.
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ export {
|
|||||||
PanelShell,
|
PanelShell,
|
||||||
Segmented,
|
Segmented,
|
||||||
ViewToggle,
|
ViewToggle,
|
||||||
type GridSize,
|
|
||||||
type PanelDensity,
|
type PanelDensity,
|
||||||
type PanelViewMode,
|
type PanelViewMode,
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Columns3,
|
||||||
Download,
|
Download,
|
||||||
Globe,
|
Globe,
|
||||||
Library,
|
Library,
|
||||||
@@ -36,9 +37,7 @@ import {
|
|||||||
GroupHeading,
|
GroupHeading,
|
||||||
PANEL,
|
PANEL,
|
||||||
PanelShell,
|
PanelShell,
|
||||||
GRID_SIZES,
|
|
||||||
ViewToggle,
|
ViewToggle,
|
||||||
type GridSize,
|
|
||||||
type PanelViewMode,
|
type PanelViewMode,
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
import { DataView, type DataColumn } from "@/components/data-view";
|
import { DataView, type DataColumn } from "@/components/data-view";
|
||||||
@@ -73,6 +72,9 @@ import { DocsLink } from "@/components/docs-link";
|
|||||||
|
|
||||||
type Section = "installed" | "browse" | "updates" | "registries";
|
type Section = "installed" | "browse" | "updates" | "registries";
|
||||||
|
|
||||||
|
const GRID_SIZES = ["auto", "2", "3", "4", "5", "6"] as const;
|
||||||
|
type GridSize = (typeof GRID_SIZES)[number];
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
view: PanelViewMode;
|
view: PanelViewMode;
|
||||||
columns: GridSize;
|
columns: GridSize;
|
||||||
@@ -362,16 +364,54 @@ function LayoutControls({
|
|||||||
onView: (next: PanelViewMode) => void;
|
onView: (next: PanelViewMode) => void;
|
||||||
onColumns: (next: GridSize) => void;
|
onColumns: (next: GridSize) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<ViewToggle
|
<>
|
||||||
view={layout.view}
|
{layout.view === "grid" && (
|
||||||
onView={onView}
|
<Select
|
||||||
columns={layout.columns}
|
value={layout.columns}
|
||||||
onColumns={onColumns}
|
onValueChange={(v) => onColumns(v as GridSize)}
|
||||||
/>
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
size="sm"
|
||||||
|
className="w-auto gap-1.5 text-xs"
|
||||||
|
title={t("plugins.manager.columns.title")}
|
||||||
|
aria-label={t("plugins.manager.columns.title")}
|
||||||
|
>
|
||||||
|
<Columns3 className="size-3.5 text-muted-foreground" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{GRID_SIZES.map((size) => (
|
||||||
|
<SelectItem key={size} value={size} className="text-xs">
|
||||||
|
{size === "auto"
|
||||||
|
? t("plugins.manager.columns.auto")
|
||||||
|
: t("plugins.manager.columns.count", { count: Number(size) })}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
<ViewToggle view={layout.view} onView={onView} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sized off the tab's own width, not the window. Cards keep a min width,
|
||||||
|
// so narrow panes drop columns on their own.
|
||||||
|
function gridStyle(columns: GridSize): React.CSSProperties {
|
||||||
|
if (columns === "auto") {
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 400px), 1fr))",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const n = Number(columns);
|
||||||
|
const gaps = (n - 1) * 12;
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: `repeat(auto-fill, minmax(max(220px, calc((100% - ${gaps}px) / ${n} - 0.5px)), 1fr))`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function PluginCollection({
|
function PluginCollection({
|
||||||
plugins,
|
plugins,
|
||||||
layout,
|
layout,
|
||||||
@@ -400,17 +440,13 @@ function PluginCollection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<DataView
|
<div className="grid gap-3" style={gridStyle(layout.columns)}>
|
||||||
items={plugins}
|
{plugins.map((p) => (
|
||||||
view="grid"
|
<div key={p.id} className="flex flex-col *:flex-1">
|
||||||
getKey={(p) => p.id}
|
{renderCard(p)}
|
||||||
renderCard={renderCard}
|
</div>
|
||||||
columns={{}}
|
))}
|
||||||
size={layout.columns}
|
</div>
|
||||||
minCardWidth={400}
|
|
||||||
listColumns={listColumns}
|
|
||||||
empty={null}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,24 +34,6 @@ describe("DataView", () => {
|
|||||||
expect(screen.queryByText("Name")).toBeNull();
|
expect(screen.queryByText("Name")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the picked size over breakpoint columns", () => {
|
|
||||||
const { container } = render(
|
|
||||||
<DataView
|
|
||||||
items={items}
|
|
||||||
view="grid"
|
|
||||||
getKey={(item) => item.id}
|
|
||||||
renderCard={(item) => <div>card {item.name}</div>}
|
|
||||||
columns={{ base: 1, md: 2 }}
|
|
||||||
size="3"
|
|
||||||
listColumns={[]}
|
|
||||||
empty={null}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
const grid = container.firstElementChild as HTMLElement;
|
|
||||||
expect(grid.className).not.toContain("md:grid-cols-2");
|
|
||||||
expect(grid.style.gridTemplateColumns).toContain("/ 3");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders a header and clickable rows in list mode", () => {
|
it("renders a header and clickable rows in list mode", () => {
|
||||||
const onRowClick = vi.fn();
|
const onRowClick = vi.fn();
|
||||||
renderView("list", onRowClick);
|
renderView("list", onRowClick);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
PanelShell,
|
PanelShell,
|
||||||
Segmented,
|
Segmented,
|
||||||
ViewToggle,
|
ViewToggle,
|
||||||
gridSizeStyle,
|
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
@@ -73,34 +72,6 @@ describe("ViewToggle", () => {
|
|||||||
fireEvent.click(screen.getByLabelText("panel.compactRows"));
|
fireEvent.click(screen.getByLabelText("panel.compactRows"));
|
||||||
expect(onDensity).toHaveBeenCalledWith("compact");
|
expect(onDensity).toHaveBeenCalledWith("compact");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the columns picker only in grid view", () => {
|
|
||||||
const props = { onView: vi.fn(), columns: "auto" as const };
|
|
||||||
const { rerender } = render(
|
|
||||||
<ViewToggle view="grid" {...props} onColumns={vi.fn()} />,
|
|
||||||
);
|
|
||||||
expect(screen.getByLabelText("panel.columns.title")).toBeTruthy();
|
|
||||||
|
|
||||||
rerender(<ViewToggle view="list" {...props} onColumns={vi.fn()} />);
|
|
||||||
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
|
||||||
|
|
||||||
rerender(<ViewToggle view="grid" {...props} />);
|
|
||||||
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("gridSizeStyle", () => {
|
|
||||||
it("fits auto cards to the pane", () => {
|
|
||||||
expect(gridSizeStyle("auto", 280).gridTemplateColumns).toBe(
|
|
||||||
"repeat(auto-fill, minmax(min(100%, 280px), 1fr))",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("splits the width for a fixed count", () => {
|
|
||||||
expect(gridSizeStyle("4").gridTemplateColumns).toContain(
|
|
||||||
"(100% - 3 * 0.5rem) / 4",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Facts", () => {
|
describe("Facts", () => {
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ vi.mock("@/look/use-look-theme", () => ({
|
|||||||
useChooseTheme: () => look.chooseTheme,
|
useChooseTheme: () => look.chooseTheme,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const fancy = vi.hoisted(() => ({ enabled: true }));
|
|
||||||
|
|
||||||
const manager = vi.hoisted(() => ({
|
const manager = vi.hoisted(() => ({
|
||||||
requestUpdate: vi.fn(),
|
requestUpdate: vi.fn(),
|
||||||
requestInstall: vi.fn(),
|
requestInstall: vi.fn(),
|
||||||
@@ -74,7 +72,7 @@ vi.mock("@/plugins/use-plugins-manager", () => ({
|
|||||||
id: "fancy-layout",
|
id: "fancy-layout",
|
||||||
name: "Fancy",
|
name: "Fancy",
|
||||||
installed: true,
|
installed: true,
|
||||||
enabled: fancy.enabled,
|
enabled: true,
|
||||||
updateAvailable: true,
|
updateAvailable: true,
|
||||||
latestVersion: "1.1.0",
|
latestVersion: "1.1.0",
|
||||||
managedByServer: false,
|
managedByServer: false,
|
||||||
@@ -117,7 +115,6 @@ const onRetry = vi.fn();
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
permissions.isAdmin = true;
|
permissions.isAdmin = true;
|
||||||
fancy.enabled = true;
|
|
||||||
for (const fn of [
|
for (const fn of [
|
||||||
...Object.values(look),
|
...Object.values(look),
|
||||||
...Object.values(manager),
|
...Object.values(manager),
|
||||||
@@ -193,31 +190,6 @@ describe("RepairView", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lets an admin turn the plugin back on and then tries again", async () => {
|
|
||||||
fancy.enabled = false;
|
|
||||||
manager.toggle.mockResolvedValue(undefined);
|
|
||||||
render(
|
|
||||||
<RepairView
|
|
||||||
status={{ ...status, reason: "disabled", error: null }}
|
|
||||||
onRetry={onRetry}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(screen.queryByText(/looks\.repair\.disable/)).toBeNull();
|
|
||||||
fireEvent.click(screen.getByText('looks.repair.enable:{"name":"Fancy"}'));
|
|
||||||
expect(manager.toggle).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ id: "fancy-layout", enabled: false }),
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(onRetry).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("offers to start a failed plugin again", () => {
|
|
||||||
fancy.enabled = false;
|
|
||||||
render(<RepairView status={status} onRetry={onRetry} />);
|
|
||||||
expect(
|
|
||||||
screen.getByText('looks.repair.startAgain:{"name":"Fancy"}'),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("lets an admin restore the bundled copy and then tries again", async () => {
|
it("lets an admin restore the bundled copy and then tries again", async () => {
|
||||||
render(<RepairView status={status} onRetry={onRetry} />);
|
render(<RepairView status={status} onRetry={onRetry} />);
|
||||||
fireEvent.click(screen.getByText('looks.repair.restore:{"name":"Fancy"}'));
|
fireEvent.click(screen.getByText('looks.repair.restore:{"name":"Fancy"}'));
|
||||||
|
|||||||
@@ -120,19 +120,6 @@ describe("layoutStatus", () => {
|
|||||||
).toMatchObject({ reason: "failed", error: "server" });
|
).toMatchObject({ reason: "failed", error: "server" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("says failed, not disabled, when the server could not start it", () => {
|
|
||||||
expect(
|
|
||||||
layoutStatus(
|
|
||||||
input({
|
|
||||||
owner: owner(
|
|
||||||
{ enabled: false, state: "failed", lastError: "bad bundle" },
|
|
||||||
"none",
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toMatchObject({ reason: "failed", error: "bad bundle" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("says incompatible when the server skipped its plugin", () => {
|
it("says incompatible when the server skipped its plugin", () => {
|
||||||
expect(
|
expect(
|
||||||
layoutStatus(
|
layoutStatus(
|
||||||
|
|||||||
@@ -183,13 +183,17 @@ describe("PluginsTab", () => {
|
|||||||
it("switches to a table and remembers it", async () => {
|
it("switches to a table and remembers it", async () => {
|
||||||
await renderTab();
|
await renderTab();
|
||||||
expect(screen.queryByRole("table")).toBeNull();
|
expect(screen.queryByRole("table")).toBeNull();
|
||||||
expect(screen.getByLabelText(en.panel.columns.title)).toBeTruthy();
|
expect(
|
||||||
|
screen.getByLabelText(en.plugins.manager.columns.title),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText(en.panel.listView));
|
fireEvent.click(screen.getByLabelText(en.panel.listView));
|
||||||
|
|
||||||
expect(screen.getByRole("table")).toBeTruthy();
|
expect(screen.getByRole("table")).toBeTruthy();
|
||||||
expect(screen.getByText(en.plugins.manager.table.version)).toBeTruthy();
|
expect(screen.getByText(en.plugins.manager.table.version)).toBeTruthy();
|
||||||
expect(screen.queryByLabelText(en.panel.columns.title)).toBeNull();
|
expect(
|
||||||
|
screen.queryByLabelText(en.plugins.manager.columns.title),
|
||||||
|
).toBeNull();
|
||||||
expect(localStorage.getItem("pluginsTab.view")).toBe("list");
|
expect(localStorage.getItem("pluginsTab.view")).toBe("list");
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|||||||
Reference in new issue
Block a user