small ui fixes

This commit is contained in:
d34dscene
2025-08-04 20:43:08 +02:00
parent 3f6f1e0bca
commit f7620e1e80
14 changed files with 169 additions and 53 deletions

BIN
.github/screenshots/dashboard.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

View File

@@ -17,7 +17,7 @@
- **Middleware Management**: Add middlewares to your routers, including rate limiting, authentication, and more.
- **Service Status**: Monitor the status of your services and see their health information.
- **DNS Providers**: Support for multiple DNS providers (currently Cloudflare, PowerDNS, Technitium) for automatic DNS record updates.
- **Agents**: New agent mode! Instead of defining your routers in the web ui, you can label your containers as usual using traefik labels. Start the agent on the machine and it will automatically set everything up for you.
- **Agents**: Instead of defining your routers in the web ui, you can label your containers as usual using traefik labels. Start the agent on the machine and it will automatically set everything up for you.
## 🚧 Disclaimer 🚧
@@ -29,7 +29,7 @@ Check out the [docs](https://mizuchi.dev/mantrae/) for more information.
### Screenshot
![Routers](./.github/screenshots/routers.png "Routers")
![Dashboard](./.github/screenshots/dashboard.png "Dashboard")
## Roadmap

View File

@@ -21,10 +21,11 @@ services:
mantrae-agent:
image: ghcr.io/mizuchilabs/mantrae-agent:latest
container_name: mantrae-agent
network_mode: host # for detecting the hostname of the machine
volumes:
- /var/run/docker.sock:/var/run/docker.sock # needed if running as container
environment:
- TOKEN=<token> # initial token from mantrae server
- TOKEN=<token> # token from mantrae server
- HOST=https://mantrae.example.com # where to reach mantrae server
restart: unless-stopped

View File

@@ -11,7 +11,7 @@ Follow these instructions to install and start using Mantrae with Traefik.
1. **Install Traefik**: Ensure you have a running instance of [Traefik](https://traefik.io/).
2. **Generate a Secret**: Create a secure, random secret key to use with Mantrae:
```bash
openssl rand -hex 32
openssl rand -base64 32
```
Copy the generated secret as you'll need it in the next steps.

View File

@@ -17,10 +17,10 @@ Get Mantrae up and running quickly with this guide. This will walk you through i
First, generate a secure secret for Mantrae:
```bash
openssl rand -hex 32
openssl rand -base64 32
```
Save this secret for the next step.
Save this secret for the next step. It has to be either of size 16, 24, or 32 bytes.
## Step 2: Run Mantrae

View File

@@ -41,7 +41,7 @@
"svelte-sonner": "^1.0.5",
"sveltekit-superforms": "^2.27.1",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^2.1.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.6",
"typescript": "^5.9.2",

22
web/pnpm-lock.yaml generated
View File

@@ -106,8 +106,8 @@ importers:
specifier: ^3.3.1
version: 3.3.1
tailwind-variants:
specifier: ^2.1.0
version: 2.1.0(tailwind-merge@3.3.1)(tailwindcss@4.1.11)
specifier: ^1.0.0
version: 1.0.0(tailwindcss@4.1.11)
tailwindcss:
specifier: ^4.1.11
version: 4.1.11
@@ -1742,18 +1742,17 @@ packages:
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@3.0.2:
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
tailwind-variants@2.1.0:
resolution: {integrity: sha512-82m0eRex0z6A3GpvfoTCpHr+wWJmbecfVZfP3mqLoDxeya5tN4mYJQZwa5Aw1hRZTedwpu1D2JizYenoEdyD8w==}
tailwind-variants@1.0.0:
resolution: {integrity: sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==}
engines: {node: '>=16.x', pnpm: '>=7.x'}
peerDependencies:
tailwind-merge: '>=3.0.0'
tailwindcss: '*'
peerDependenciesMeta:
tailwind-merge:
optional: true
tailwindcss@4.1.11:
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
@@ -3518,13 +3517,14 @@ snapshots:
tabbable@6.2.0: {}
tailwind-merge@3.0.2: {}
tailwind-merge@3.3.1: {}
tailwind-variants@2.1.0(tailwind-merge@3.3.1)(tailwindcss@4.1.11):
tailwind-variants@1.0.0(tailwindcss@4.1.11):
dependencies:
tailwind-merge: 3.0.2
tailwindcss: 4.1.11
optionalDependencies:
tailwind-merge: 3.3.1
tailwindcss@4.1.11: {}

View File

@@ -5,6 +5,7 @@
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import * as ToggleGroup from '$lib/components/ui/toggle-group/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import {
DnsProviderType,
@@ -200,16 +201,23 @@
<Label class="text-sm">Zone Type</Label>
<p class="text-xs text-muted-foreground">DNS zone configuration type</p>
</div>
<CustomSwitch
variant="text"
textLabels={{ checked: 'Forwarder', unchecked: 'Primary' }}
checked={(item.config?.zoneType || 'primary') === 'forwarder'}
onCheckedChange={(value) => {
<ToggleGroup.Root
type="single"
size="sm"
value={item.config?.zoneType}
onValueChange={(value) => {
if (item.config === undefined) item.config = {} as DnsProviderConfig;
item.config.zoneType = value ? 'forwarder' : 'primary';
item.config.zoneType = value;
}}
size="md"
/>
class="border"
>
<ToggleGroup.Item value="primary" aria-label="Toggle primary">
<span class="text-xs">Primary</span>
</ToggleGroup.Item>
<ToggleGroup.Item value="forwarder" aria-label="Toggle forwarder" class="px-2">
<span class="text-xs">Forwarder</span>
</ToggleGroup.Item>
</ToggleGroup.Root>
</div>
{/if}
</div>

View File

@@ -0,0 +1,10 @@
import Root from "./toggle-group.svelte";
import Item from "./toggle-group-item.svelte";
export {
Root,
Item,
//
Root as ToggleGroup,
Item as ToggleGroupItem,
};

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
import { getToggleGroupCtx } from "./toggle-group.svelte";
import { cn } from "$lib/utils.js";
import { type ToggleVariants, toggleVariants } from "$lib/components/ui/toggle/index.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size,
variant,
...restProps
}: ToggleGroupPrimitive.ItemProps & ToggleVariants = $props();
const ctx = getToggleGroupCtx();
</script>
<ToggleGroupPrimitive.Item
bind:ref
data-slot="toggle-group-item"
data-variant={ctx.variant || variant}
data-size={ctx.size || size}
class={cn(
toggleVariants({
variant: ctx.variant || variant,
size: ctx.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{value}
{...restProps}
/>

View File

@@ -0,0 +1,47 @@
<script lang="ts" module>
import { getContext, setContext } from "svelte";
import type { ToggleVariants } from "$lib/components/ui/toggle/index.js";
export function setToggleGroupCtx(props: ToggleVariants) {
setContext("toggleGroup", props);
}
export function getToggleGroupCtx() {
return getContext<ToggleVariants>("toggleGroup");
}
</script>
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size = "default",
variant = "default",
...restProps
}: ToggleGroupPrimitive.RootProps & ToggleVariants = $props();
setToggleGroupCtx({
variant,
size,
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<ToggleGroupPrimitive.Root
bind:value={value as never}
bind:ref
data-slot="toggle-group"
data-variant={variant}
data-size={size}
class={cn(
"group/toggle-group data-[variant=outline]:shadow-xs flex w-fit items-center rounded-md",
className
)}
{...restProps}
/>

View File

@@ -1,13 +1,13 @@
import Root from './toggle.svelte';
import Root from "./toggle.svelte";
export {
toggleVariants,
type ToggleSize,
type ToggleVariant,
type ToggleVariants
} from './toggle.svelte';
type ToggleVariants,
} from "./toggle.svelte";
export {
Root,
//
Root as Toggle
Root as Toggle,
};

View File

@@ -1,41 +1,41 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
import { type VariantProps, tv } from "tailwind-variants";
export const toggleVariants = tv({
base: "hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-transparent',
default: "bg-transparent",
outline:
'border-input shadow-xs hover:bg-accent hover:text-accent-foreground border bg-transparent'
"border-input shadow-xs hover:bg-accent hover:text-accent-foreground border bg-transparent",
},
size: {
default: 'h-9 min-w-9 px-2',
sm: 'h-8 min-w-8 px-1.5',
lg: 'h-10 min-w-10 px-2.5'
}
default: "h-9 min-w-9 px-2",
sm: "h-8 min-w-8 px-1.5",
lg: "h-10 min-w-10 px-2.5",
},
},
defaultVariants: {
variant: 'default',
size: 'default'
}
variant: "default",
size: "default",
},
});
export type ToggleVariant = VariantProps<typeof toggleVariants>['variant'];
export type ToggleSize = VariantProps<typeof toggleVariants>['size'];
export type ToggleVariant = VariantProps<typeof toggleVariants>["variant"];
export type ToggleSize = VariantProps<typeof toggleVariants>["size"];
export type ToggleVariants = VariantProps<typeof toggleVariants>;
</script>
<script lang="ts">
import { Toggle as TogglePrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { Toggle as TogglePrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
pressed = $bindable(false),
class: className,
size = 'default',
variant = 'default',
size = "default",
variant = "default",
...restProps
}: TogglePrimitive.RootProps & {
variant?: ToggleVariant;

View File

@@ -6,6 +6,7 @@
import { Badge } from '$lib/components/ui/badge/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import Progress from '$lib/components/ui/progress/progress.svelte';
import { Separator } from '$lib/components/ui/separator/index.js';
import TraefikConnection from '$lib/components/utils/TraefikConnection.svelte';
import type { Profile } from '$lib/gen/mantrae/v1/profile_pb';
@@ -40,8 +41,7 @@
Server,
Shield,
TrendingUp,
Users,
Wifi
Users
} from '@lucide/svelte';
let onlineAgents = $derived.by(() => {
@@ -138,10 +138,26 @@
<Bot class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-3xl font-bold">{onlineAgents}</div>
<div class="mt-2 flex items-center text-sm">
<Wifi class="mr-1 h-3 w-3 text-blue-500" />
<span class="text-blue-500">Online now</span>
<div class="space-y-3">
<div class="flex items-baseline gap-2">
<span class="text-3xl font-bold text-primary">{onlineAgents}</span>
<span class="text-lg text-muted-foreground">/</span>
<span class="text-lg font-medium text-muted-foreground">{$agents?.length}</span>
</div>
<div class="space-y-2">
<Progress
value={$agents?.length ? (Number(onlineAgents) / $agents.length) * 100 : 0}
class="h-2"
/>
<div class="flex justify-between text-xs text-muted-foreground">
<span>{Number(onlineAgents)} online</span>
<span>
{$agents?.length ? Math.round((Number(onlineAgents) / $agents.length) * 100) : 0}
%
</span>
</div>
</div>
</div>
</Card.Content>
<div
@@ -186,7 +202,7 @@
<div class="text-3xl font-bold">{$users?.length}</div>
<div class="mt-2 flex items-center text-sm">
<Shield class="mr-1 h-3 w-3 text-purple-500" />
<span class="text-muted-foreground">Access managed</span>
<span class="text-muted-foreground">Active users</span>
</div>
</Card.Content>
<div