mirror of
https://github.com/warp-tech/warpgate.git
synced 2025-09-08 15:44:25 +08:00
UI tweaks
This commit is contained in:
parent
8370c5459f
commit
ee499f2d8b
6 changed files with 252 additions and 126 deletions
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
<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 () {
|
||||
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}
|
||||
<Loadable promise={initPromise}>
|
||||
{#if parameters}
|
||||
<label
|
||||
for="allowOwnCredentialManagement"
|
||||
class="d-flex align-items-center"
|
||||
|
@ -38,6 +37,5 @@
|
|||
checked={parameters.allowOwnCredentialManagement} />
|
||||
<div>Allow users to manage their own credentials</div>
|
||||
</label>
|
||||
{/if}
|
||||
</Loadable>
|
||||
</div>
|
||||
{/if}
|
||||
</Loadable>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
.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 h5 {
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
46
warpgate-web/src/common/RadioButton.svelte
Normal file
46
warpgate-web/src/common/RadioButton.svelte
Normal 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>
|
Loading…
Add table
Reference in a new issue