UI tweaks

This commit is contained in:
Eugene 2025-05-26 20:22:39 +02:00
parent 8370c5459f
commit ee499f2d8b
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
6 changed files with 252 additions and 126 deletions

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { serverInfo, reloadServerInfo } from 'gateway/lib/store'
import Router, { link } from 'svelte-spa-router'
import Router, { link, type WrappedComponent } from 'svelte-spa-router'
import active from 'svelte-spa-router/active'
import { wrap } from 'svelte-spa-router/wrap'
import ThemeSwitcher from 'common/ThemeSwitcher.svelte'
@ -15,7 +15,7 @@
const initPromise = init()
const routes = {
const routes: Record<string, WrappedComponent> = {
'/': wrap({
asyncComponent: () => import('./Home.svelte') as any,
}),
@ -25,56 +25,18 @@
'/recordings/:id': wrap({
asyncComponent: () => import('./Recording.svelte') as any,
}),
'/config/targets/create': wrap({
asyncComponent: () => import('./config/CreateTarget.svelte') as any,
}),
'/config/targets/:id': wrap({
asyncComponent: () => import('./config/targets/Target.svelte') as any,
}),
'/config/roles/create': wrap({
asyncComponent: () => import('./config/CreateRole.svelte') as any,
}),
'/config/roles/:id': wrap({
asyncComponent: () => import('./config/Role.svelte') as any,
}),
'/config/users/create': wrap({
asyncComponent: () => import('./config/CreateUser.svelte') as any,
}),
'/config/users/:id': wrap({
asyncComponent: () => import('./config/User.svelte') as any,
}),
'/log': wrap({
asyncComponent: () => import('./Log.svelte') as any,
}),
'/config': wrap({
asyncComponent: () => import('./config/Config.svelte') as any,
}),
'/config/parameters': wrap({
asyncComponent: () => import('./config/Parameters.svelte') as any,
}),
'/config/users': wrap({
asyncComponent: () => import('./config/Users.svelte') as any,
}),
'/config/roles': wrap({
asyncComponent: () => import('./config/Roles.svelte') as any,
}),
'/config/targets': wrap({
asyncComponent: () => import('./config/targets/Targets.svelte') as any,
}),
'/config/ssh': wrap({
asyncComponent: () => import('./config/SSHKeys.svelte') as any,
}),
'/config/tickets': wrap({
asyncComponent: () => import('./config/Tickets.svelte') as any,
}),
'/config/tickets/create': wrap({
asyncComponent: () => import('./config/CreateTicket.svelte') as any,
}),
}
routes['/config/*'] = routes['/config']!
</script>
<Loadable promise={initPromise}>
<div class="app container">
<div class="app container-lg">
<header>
<a href="/@warpgate" class="d-flex logo-link me-4">
<Brand />

View file

@ -1,41 +1,142 @@
<script lang="ts">
import NavListItem from 'common/NavListItem.svelte'
import { wrap } from 'svelte-spa-router/wrap'
import Router from 'svelte-spa-router'
const routes = {
'/targets/create': wrap({
asyncComponent: () => import('./CreateTarget.svelte') as any,
}),
'/targets/:id': wrap({
asyncComponent: () => import('./targets/Target.svelte') as any,
}),
'/roles/create': wrap({
asyncComponent: () => import('./CreateRole.svelte') as any,
}),
'/roles/:id': wrap({
asyncComponent: () => import('./Role.svelte') as any,
}),
'/users/create': wrap({
asyncComponent: () => import('./CreateUser.svelte') as any,
}),
'/users/:id': wrap({
asyncComponent: () => import('./User.svelte') as any,
}),
'/parameters': wrap({
asyncComponent: () => import('./Parameters.svelte') as any,
}),
'/users': wrap({
asyncComponent: () => import('./Users.svelte') as any,
}),
'/roles': wrap({
asyncComponent: () => import('./Roles.svelte') as any,
}),
'/targets': wrap({
asyncComponent: () => import('./targets/Targets.svelte') as any,
}),
'/ssh': wrap({
asyncComponent: () => import('./SSHKeys.svelte') as any,
}),
'/tickets': wrap({
asyncComponent: () => import('./Tickets.svelte') as any,
}),
'/tickets/create': wrap({
asyncComponent: () => import('./CreateTicket.svelte') as any,
}),
}
let sidebarMode = $state(false)
</script>
<div class="container-max-md">
{#snippet navItems()}
<NavListItem
class="mb-2"
title="Targets"
description="Destinations for users to connect to"
href="/config/targets"
small={sidebarMode}
/>
<NavListItem
class="mb-2"
title="Users"
description="Manage accounts and credentials"
href="/config/users"
small={sidebarMode}
/>
<NavListItem
class="mb-2"
title="Roles"
description="Group users together"
href="/config/roles"
small={sidebarMode}
/>
<NavListItem
class="mb-2"
title="Tickets"
description="Temporary access credentials"
href="/config/tickets"
small={sidebarMode}
/>
<NavListItem
class="mb-2"
title="SSH keys"
description="Own keys and known hosts"
href="/config/ssh"
small={sidebarMode}
/>
<NavListItem
class="mb-2"
title="Global parameters"
description="Change instance-wide settings"
href="/config/parameters"
small={sidebarMode}
/>
{/snippet}
<div class="wrapper" class:d-none={!sidebarMode}>
<div class="sidebar">
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
{@render navItems()}
</div>
<div class="main">
<Router {routes} prefix="/config" on:routeLoading={e => {
sidebarMode = e.detail.route !== ''
}} />
</div>
</div>
<div class="container-max-md" class:d-none={sidebarMode}>
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
{@render navItems()}
</div>
<style lang="scss">
$sb-w: 200px;
$sb-m: 30px;
.wrapper {
display: flex;
gap: $sb-m;
> .sidebar {
width: $sb-w;
flex: none;
}
> .main {
flex: 1 0 0;
}
}
@media (max-width: #{720px + $sb-m + $sb-w}) {
.sidebar {
display: none;
}
}
</style>

View file

@ -5,6 +5,7 @@
import { stringifyError } from 'common/errors'
import Alert from 'common/sveltestrap-s5-ports/Alert.svelte'
import { TargetKind } from 'gateway/lib/api'
import RadioButton from 'common/RadioButton.svelte'
let error: string|null = $state(null)
let name = $state('')
@ -69,6 +70,12 @@
}
}
const kinds: { name: string, value: TargetKind }[] = [
{ name: 'SSH', value: TargetKind.Ssh },
{ name: 'HTTP', value: TargetKind.Http },
{ name: 'MySQL', value: TargetKind.MySql },
{ name: 'PostgreSQL', value: TargetKind.Postgres },
]
</script>
<div class="container-max-md">
@ -91,34 +98,13 @@
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="mb-2">Type</label>
<ButtonGroup class="w-100 mb-3">
<Button
active={type === TargetKind.Ssh}
on:click={e => {
type = TargetKind.Ssh
e.preventDefault()
}}
>SSH</Button>
<Button
active={type === TargetKind.Http}
on:click={e => {
type = TargetKind.Http
e.preventDefault()
}}
>HTTP</Button>
<Button
active={type === TargetKind.MySql}
on:click={e => {
type = TargetKind.MySql
e.preventDefault()
}}
>MySQL</Button>
<Button
active={type === TargetKind.Postgres}
on:click={e => {
type = TargetKind.Postgres
e.preventDefault()
}}
>PostgreSQL</Button>
{#each kinds as kind}
<RadioButton
label={kind.name}
value={kind.value}
bind:group={type}
/>
{/each}
</ButtonGroup>
<FormGroup floating label="Name">

View file

@ -1,43 +1,41 @@
<script lang="ts">
import { Input } from '@sveltestrap/sveltestrap'
import { api, type ParameterValues } from 'admin/lib/api'
import Loadable from 'common/Loadable.svelte'
import { Input } from '@sveltestrap/sveltestrap'
import { api, type ParameterValues } from 'admin/lib/api'
import Loadable from 'common/Loadable.svelte'
let parameters: ParameterValues | undefined = $state()
const initPromise = init()
let parameters: ParameterValues | undefined = $state()
const initPromise = init()
async function init () {
parameters = await api.getParameters({})
}
async function init () {
parameters = await api.getParameters({})
}
</script>
<div class="page-summary-bar">
<h1>global parameters</h1>
<h1>global parameters</h1>
</div>
<div class="container-max-md">
<Loadable promise={initPromise}>
{#if parameters}
<label
for="allowOwnCredentialManagement"
class="d-flex align-items-center"
>
<Input
id="allowOwnCredentialManagement"
class="mb-0 me-2"
type="switch"
on:change={() => {
parameters!.allowOwnCredentialManagement = !parameters!.allowOwnCredentialManagement
api.updateParameters({
parameterUpdate: {
allowOwnCredentialManagement: parameters!.allowOwnCredentialManagement,
},
})
}}
checked={parameters.allowOwnCredentialManagement} />
<div>Allow users to manage their own credentials</div>
</label>
{/if}
</Loadable>
</div>
<Loadable promise={initPromise}>
{#if parameters}
<label
for="allowOwnCredentialManagement"
class="d-flex align-items-center"
>
<Input
id="allowOwnCredentialManagement"
class="mb-0 me-2"
type="switch"
on:change={() => {
parameters!.allowOwnCredentialManagement = !parameters!.allowOwnCredentialManagement
api.updateParameters({
parameterUpdate: {
allowOwnCredentialManagement: parameters!.allowOwnCredentialManagement,
},
})
}}
checked={parameters.allowOwnCredentialManagement} />
<div>Allow users to manage their own credentials</div>
</label>
{/if}
</Loadable>

View file

@ -2,27 +2,40 @@
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
import { link } from 'svelte-spa-router'
import active from 'svelte-spa-router/active'
import { classnames } from './sveltestrap-s5-ports/_sveltestrapUtils'
interface Props {
class?: string,
title: string
description?: string
href: string
small?: boolean
}
let {
title,
'class': className,
description,
href,
small,
}: Props = $props()
let classes = $derived(classnames(
className,
'link',
small ? 'sm' : false,
))
</script>
<a
class="link"
class={classes}
href={href}
use:link
use:active
>
<div class="text">
<h5 class="title">{title}</h5>
<div class="title">{title}</div>
{#if description}
<div class="description text-muted">{description}</div>
{/if}
@ -39,7 +52,7 @@
display: flex;
width: 100%;
text-decoration: none;
padding: 1rem 1.5rem;
padding: 0.8rem 1.5rem 1rem;
border-radius: var(--bs-border-radius);
align-items: center;
@ -47,34 +60,54 @@
flex-grow: 1;
}
&:hover {
&:hover, &.active {
background: var(--bs-list-group-action-hover-bg);
h5 {
.title {
color: var(--bs-list-group-action-hover-color);
}
}
&:active {
background: var(--bs-list-group-action-active-bg);
h5 {
.title {
color: var(--bs-list-group-action-active-color);
}
}
.title {
margin-bottom: 0.25rem;
font-size: 1.25rem;
text-decoration: underline;
text-decoration-color: var(--wg-link-underline-color);
text-underline-offset: 2px;
}
&.link:hover .title {
text-decoration-color: var(--wg-link-hover-underline-color);
}
.description {
text-decoration: none;
line-height: 1rem;
font-size: 0.9rem;
}
&.sm {
padding: 0.5rem 1rem;
.title {
font-size: 1rem;
}
.description {
font-size: 0.8rem;
}
.icon {
display: none;
}
}
}
h5 {
margin-bottom: 0.25rem;
text-decoration: underline;
text-decoration-color: var(--wg-link-underline-color);
text-underline-offset: 2px;
}
.link:hover h5 {
text-decoration-color: var(--wg-link-hover-underline-color);
}
.description {
text-decoration: none;
}
</style>

View file

@ -0,0 +1,46 @@
<script lang="ts" generics="T">
import { Button, Input } from '@sveltestrap/sveltestrap'
import { classnames } from './sveltestrap-s5-ports/_sveltestrapUtils'
type Props = {
group: T,
value: T,
label: string,
} & Button['$$prop_def']
let {
group = $bindable(),
value,
label,
...rest
}: Props = $props()
let classes = $derived(classnames(
'btn-radio-button',
group === value ? 'active': false,
))
</script>
<Button on:click={e => {
group = value
e.preventDefault()
}} class={classes} {...rest}>
<Input
{label}
type="radio"
{value}
bind:group={group}
on:click={e => e.preventDefault()}
/>
</Button>
<style lang="scss">
:global .btn-radio-button {
text-align: left;
label {
margin-left: .75rem;
}
}
</style>