mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
ui: added search boxes - #761
This commit is contained in:
parent
5af6cf3597
commit
a38fd2bbb1
|
@ -1,10 +1,13 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use poem::web::Data;
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::param::{Path, Query};
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Object, OpenApi};
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryOrder, Set};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
use warpgate_common::{Role as RoleConfig, WarpgateError};
|
||||
|
@ -38,11 +41,18 @@ impl ListApi {
|
|||
async fn api_get_all_roles(
|
||||
&self,
|
||||
db: Data<&Arc<Mutex<DatabaseConnection>>>,
|
||||
search: Query<Option<String>>,
|
||||
) -> poem::Result<GetRolesResponse> {
|
||||
let db = db.lock().await;
|
||||
|
||||
let roles = Role::Entity::find()
|
||||
.order_by_asc(Role::Column::Name)
|
||||
let mut roles = Role::Entity::find().order_by_asc(Role::Column::Name);
|
||||
|
||||
if let Some(ref search) = *search {
|
||||
let search = format!("%{}%", search);
|
||||
roles = roles.filter(Role::Column::Name.like(&*search));
|
||||
}
|
||||
|
||||
let roles = roles
|
||||
.all(&*db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use poem::web::Data;
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::param::{Path, Query};
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Object, OpenApi};
|
||||
use sea_orm::{
|
||||
|
@ -43,14 +43,18 @@ impl ListApi {
|
|||
async fn api_get_all_targets(
|
||||
&self,
|
||||
db: Data<&Arc<Mutex<DatabaseConnection>>>,
|
||||
search: Query<Option<String>>,
|
||||
) -> poem::Result<GetTargetsResponse> {
|
||||
let db = db.lock().await;
|
||||
|
||||
let targets = Target::Entity::find()
|
||||
.order_by_asc(Target::Column::Name)
|
||||
.all(&*db)
|
||||
.await
|
||||
.map_err(WarpgateError::from)?;
|
||||
let mut targets = Target::Entity::find().order_by_asc(Target::Column::Name);
|
||||
|
||||
if let Some(ref search) = *search {
|
||||
let search = format!("%{}%", search);
|
||||
targets = targets.filter(Target::Column::Name.like(&*search));
|
||||
}
|
||||
|
||||
let targets = targets.all(&*db).await.map_err(WarpgateError::from)?;
|
||||
|
||||
let targets: Result<Vec<TargetConfig>, _> =
|
||||
targets.into_iter().map(|t| t.try_into()).collect();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use poem::web::Data;
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::param::{Path, Query};
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Object, OpenApi};
|
||||
use sea_orm::{
|
||||
|
@ -46,14 +46,18 @@ impl ListApi {
|
|||
async fn api_get_all_users(
|
||||
&self,
|
||||
db: Data<&Arc<Mutex<DatabaseConnection>>>,
|
||||
search: Query<Option<String>>,
|
||||
) -> poem::Result<GetUsersResponse> {
|
||||
let db = db.lock().await;
|
||||
|
||||
let users = User::Entity::find()
|
||||
.order_by_asc(User::Column::Username)
|
||||
.all(&*db)
|
||||
.await
|
||||
.map_err(WarpgateError::from)?;
|
||||
let mut users = User::Entity::find().order_by_asc(User::Column::Username);
|
||||
|
||||
if let Some(ref search) = *search {
|
||||
let search = format!("%{}%", search);
|
||||
users = users.filter(User::Column::Username.like(&*search));
|
||||
}
|
||||
|
||||
let users = users.all(&*db).await.map_err(WarpgateError::from)?;
|
||||
|
||||
let users: Result<Vec<UserConfig>, _> = users.into_iter().map(|t| t.try_into()).collect();
|
||||
let users = users.map_err(WarpgateError::from)?;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use futures::{stream, StreamExt};
|
||||
use poem::web::Data;
|
||||
use poem_openapi::param::Query;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Object, OpenApi};
|
||||
use serde::Serialize;
|
||||
|
@ -36,11 +37,20 @@ impl Api {
|
|||
&self,
|
||||
services: Data<&Services>,
|
||||
auth: Data<&SessionAuthorization>,
|
||||
search: Query<Option<String>>,
|
||||
) -> poem::Result<GetTargetsResponse> {
|
||||
let targets = {
|
||||
let mut targets = {
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
config_provider.list_targets().await?
|
||||
};
|
||||
|
||||
if let Some(ref search) = *search {
|
||||
targets = targets
|
||||
.into_iter()
|
||||
.filter(|t| t.name.contains(search))
|
||||
.collect()
|
||||
}
|
||||
|
||||
let mut targets = stream::iter(targets)
|
||||
.filter(|t| {
|
||||
let services = services.clone();
|
||||
|
|
|
@ -1,8 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { api } from 'admin/lib/api'
|
||||
import DelayedSpinner from 'common/DelayedSpinner.svelte'
|
||||
import { link } from 'svelte-spa-router'
|
||||
import { Alert } from 'sveltestrap'
|
||||
import { Observable, from, map } from 'rxjs'
|
||||
import { Role, Target, User, api } from 'admin/lib/api'
|
||||
import ItemList, { LoadOptions, PaginatedResponse } from 'common/ItemList.svelte'
|
||||
import { link } from 'svelte-spa-router'
|
||||
|
||||
function getTargets (options: LoadOptions): Observable<PaginatedResponse<Target>> {
|
||||
return from(api.getTargets({
|
||||
search: options.search,
|
||||
})).pipe(map(targets => ({
|
||||
items: targets,
|
||||
offset: 0,
|
||||
total: targets.length,
|
||||
})))
|
||||
}
|
||||
|
||||
function getUsers (options: LoadOptions): Observable<PaginatedResponse<User>> {
|
||||
return from(api.getUsers({
|
||||
search: options.search,
|
||||
})).pipe(map(targets => ({
|
||||
items: targets,
|
||||
offset: 0,
|
||||
total: targets.length,
|
||||
})))
|
||||
}
|
||||
|
||||
function getRoles (options: LoadOptions): Observable<PaginatedResponse<Role>> {
|
||||
return from(api.getRoles({
|
||||
search: options.search,
|
||||
})).pipe(map(targets => ({
|
||||
items: targets,
|
||||
offset: 0,
|
||||
total: targets.length,
|
||||
})))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
|
@ -17,39 +47,32 @@ import { Alert } from 'sveltestrap'
|
|||
</a>
|
||||
</div>
|
||||
|
||||
{#await api.getTargets()}
|
||||
<DelayedSpinner />
|
||||
{:then targets}
|
||||
<div class="list-group list-group-flush">
|
||||
{#each targets as target}
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/targets/{target.id}"
|
||||
use:link>
|
||||
<strong class="me-auto">
|
||||
{target.name}
|
||||
</strong>
|
||||
<small class="text-muted ms-auto">
|
||||
{#if target.options.kind === 'Http'}
|
||||
HTTP
|
||||
{/if}
|
||||
{#if target.options.kind === 'MySql'}
|
||||
MySQL
|
||||
{/if}
|
||||
{#if target.options.kind === 'Ssh'}
|
||||
SSH
|
||||
{/if}
|
||||
{#if target.options.kind === 'WebAdmin'}
|
||||
This web admin interface
|
||||
{/if}
|
||||
</small>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:catch error}
|
||||
<Alert color="danger">{error}</Alert>
|
||||
{/await}
|
||||
<ItemList load={getTargets} showSearch={true}>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
slot="item" let:item={target}
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/targets/{target.id}"
|
||||
use:link>
|
||||
<strong class="me-auto">
|
||||
{target.name}
|
||||
</strong>
|
||||
<small class="text-muted ms-auto">
|
||||
{#if target.options.kind === 'Http'}
|
||||
HTTP
|
||||
{/if}
|
||||
{#if target.options.kind === 'MySql'}
|
||||
MySQL
|
||||
{/if}
|
||||
{#if target.options.kind === 'Ssh'}
|
||||
SSH
|
||||
{/if}
|
||||
{#if target.options.kind === 'WebAdmin'}
|
||||
This web admin interface
|
||||
{/if}
|
||||
</small>
|
||||
</a>
|
||||
</ItemList>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6 pe-4">
|
||||
|
@ -63,25 +86,18 @@ import { Alert } from 'sveltestrap'
|
|||
</a>
|
||||
</div>
|
||||
|
||||
{#await api.getUsers()}
|
||||
<DelayedSpinner />
|
||||
{:then users}
|
||||
<div class="list-group list-group-flush">
|
||||
{#each users as user}
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/users/{user.id}"
|
||||
use:link>
|
||||
<strong class="me-auto">
|
||||
{user.username}
|
||||
</strong>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:catch error}
|
||||
<Alert color="danger">{error}</Alert>
|
||||
{/await}
|
||||
<ItemList load={getUsers} showSearch={true}>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
slot="item" let:item={user}
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/users/{user.id}"
|
||||
use:link>
|
||||
<strong class="me-auto">
|
||||
{user.username}
|
||||
</strong>
|
||||
</a>
|
||||
</ItemList>
|
||||
|
||||
<div class="page-summary-bar mt-4">
|
||||
<h1>Roles</h1>
|
||||
|
@ -93,25 +109,18 @@ import { Alert } from 'sveltestrap'
|
|||
</a>
|
||||
</div>
|
||||
|
||||
{#await api.getRoles()}
|
||||
<DelayedSpinner />
|
||||
{:then roles}
|
||||
<div class="list-group list-group-flush">
|
||||
{#each roles as role}
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/roles/{role.id}"
|
||||
use:link>
|
||||
<strong class="me-auto">
|
||||
{role.name}
|
||||
</strong>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:catch error}
|
||||
<Alert color="danger">{error}</Alert>
|
||||
{/await}
|
||||
<ItemList load={getRoles} showSearch={true}>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
slot="item" let:item={role}
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/roles/{role.id}"
|
||||
use:link>
|
||||
<strong class="me-auto">
|
||||
{role.name}
|
||||
</strong>
|
||||
</a>
|
||||
</ItemList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
{/if}
|
||||
|
||||
<ItemList load={loadSessions} pageSize={100}>
|
||||
<div slot="header" class="d-flex align-items-center mb-1">
|
||||
<div slot="header" class="d-flex align-items-center mb-1 w-100">
|
||||
<div class="ms-auto"></div>
|
||||
<Input class="ms-3" type="switch" label="Active only" bind:checked={$showActiveOnly} />
|
||||
<Input class="ms-3" type="switch" label="Logged in only" bind:checked={$showLoggedInOnly} />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Warpgate Web Admin",
|
||||
"version": "0.7.0"
|
||||
"version": "0.7.1"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -207,6 +207,18 @@
|
|||
},
|
||||
"/roles": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "search",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
|
@ -366,6 +378,18 @@
|
|||
},
|
||||
"/targets": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "search",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
|
@ -636,6 +660,18 @@
|
|||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "search",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts" context="module">
|
||||
export interface LoadOptions {
|
||||
search?: string
|
||||
offset: number
|
||||
limit: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
|
@ -13,30 +14,49 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
import { Subject, switchMap, map, Observable, distinctUntilChanged, share } from 'rxjs'
|
||||
import { Subject, switchMap, map, Observable, distinctUntilChanged, share, combineLatest, tap, debounceTime } from 'rxjs'
|
||||
import Pagination from './Pagination.svelte'
|
||||
import { observe } from 'svelte-observable'
|
||||
import { Input } from 'sveltestrap'
|
||||
import DelayedSpinner from './DelayedSpinner.svelte'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
||||
type T = $$Generic
|
||||
|
||||
export let page = 0
|
||||
export let pageSize = 100
|
||||
export let pageSize: number|undefined = undefined
|
||||
export let load: (_: LoadOptions) => Observable<PaginatedResponse<T>>
|
||||
export let showSearch = false
|
||||
|
||||
let filter = ''
|
||||
let loaded = false
|
||||
|
||||
const page$ = new Subject<number>()
|
||||
const filter$ = new Subject<string>()
|
||||
|
||||
const responses = page$.pipe(
|
||||
const responses = combineLatest([
|
||||
page$,
|
||||
filter$.pipe(
|
||||
tap(() => {
|
||||
loaded = false
|
||||
}),
|
||||
debounceTime(200),
|
||||
),
|
||||
]).pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap(p => {
|
||||
switchMap(([p, f]) => {
|
||||
page = p
|
||||
loaded = false
|
||||
return load({
|
||||
offset: p * pageSize,
|
||||
search: f,
|
||||
offset: p * (pageSize ?? 0),
|
||||
limit: pageSize,
|
||||
})
|
||||
}),
|
||||
share(),
|
||||
tap(() => {
|
||||
loaded = true
|
||||
}),
|
||||
)
|
||||
|
||||
const total = observe<number>(responses.pipe(map(x => x.total)), 0)
|
||||
|
@ -44,16 +64,27 @@
|
|||
|
||||
onDestroy(() => {
|
||||
page$.complete()
|
||||
filter$.complete()
|
||||
})
|
||||
|
||||
$: page$.next(page)
|
||||
$: filter$.next(filter)
|
||||
|
||||
filter$.subscribe(() => {
|
||||
page = 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="d-flex mb-2" hidden={!loaded}>
|
||||
{#if showSearch}
|
||||
<Input bind:value={filter} placeholder="Search..." class="flex-grow-1 border-0" />
|
||||
{/if}
|
||||
<slot name="header" items={items} />
|
||||
</div>
|
||||
{#await $items}
|
||||
<DelayedSpinner />
|
||||
{:then items}
|
||||
{#if items}
|
||||
<slot name="header" items={items} />
|
||||
<div class="list-group list-group-flush mb-3">
|
||||
{#each items as item}
|
||||
<slot name="item" item={item} />
|
||||
|
@ -63,10 +94,16 @@
|
|||
{:else}
|
||||
<DelayedSpinner />
|
||||
{/if}
|
||||
|
||||
{#if filter && loaded && !items?.length}
|
||||
<em>
|
||||
Nothing found
|
||||
</em>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
{#await $total then total}
|
||||
{#if total > pageSize}
|
||||
{#if pageSize && total > pageSize}
|
||||
<Pagination total={total} bind:page={page} pageSize={pageSize} />
|
||||
{/if}
|
||||
{/await}
|
||||
|
|
|
@ -1,100 +1,90 @@
|
|||
<script lang="ts">
|
||||
import { Observable, from, map } from 'rxjs'
|
||||
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import ConnectionInstructions from 'common/ConnectionInstructions.svelte'
|
||||
import DelayedSpinner from 'common/DelayedSpinner.svelte'
|
||||
import ItemList, { LoadOptions, PaginatedResponse } from 'common/ItemList.svelte'
|
||||
import { api, TargetSnapshot, TargetKind } from 'gateway/lib/api'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Fa from 'svelte-fa'
|
||||
import { Modal, ModalBody, ModalHeader } from 'sveltestrap'
|
||||
import { serverInfo } from './lib/store'
|
||||
import { firstBy } from 'thenby'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let targets: TargetSnapshot[]|undefined
|
||||
let haveAdminTarget = false
|
||||
let selectedTarget: TargetSnapshot|undefined
|
||||
|
||||
async function init () {
|
||||
targets = await api.getTargets()
|
||||
haveAdminTarget = targets.some(t => t.kind === TargetKind.WebAdmin)
|
||||
targets = targets.filter(t => t.kind !== TargetKind.WebAdmin)
|
||||
function loadTargets (options: LoadOptions): Observable<PaginatedResponse<TargetSnapshot>> {
|
||||
return from(api.getTargets({ search: options.search })).pipe(
|
||||
map(result => {
|
||||
result = result.sort(
|
||||
firstBy<TargetSnapshot, boolean>(x => x.kind !== TargetKind.WebAdmin)
|
||||
.thenBy(x => x.name.toLowerCase())
|
||||
)
|
||||
return {
|
||||
items: result,
|
||||
offset: 0,
|
||||
total: result.length,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function selectTarget (target: TargetSnapshot) {
|
||||
if (target.kind === TargetKind.Http) {
|
||||
if (target.kind === TargetKind.WebAdmin) {
|
||||
loadURL('/@warpgate/admin')
|
||||
} else if (target.kind === TargetKind.Http) {
|
||||
loadURL(`/?warpgate-target=${target.name}`)
|
||||
} else {
|
||||
selectedTarget = target
|
||||
}
|
||||
}
|
||||
|
||||
function selectAdminTarget () {
|
||||
loadURL('/@warpgate/admin')
|
||||
}
|
||||
|
||||
function loadURL (url: string) {
|
||||
dispatch('navigation')
|
||||
location.href = url
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
</script>
|
||||
|
||||
{#if targets}
|
||||
<div class="list-group list-group-flush">
|
||||
{#if haveAdminTarget}
|
||||
<a
|
||||
class="list-group-item list-group-item-action target-item"
|
||||
href="/@warpgate/admin"
|
||||
on:click|preventDefault={e => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
selectAdminTarget()
|
||||
}}
|
||||
>
|
||||
<span class="me-auto">
|
||||
Manage Warpgate
|
||||
</span>
|
||||
<Fa icon={faArrowRight} fw />
|
||||
</a>
|
||||
{/if}
|
||||
{#each targets as target}
|
||||
<a
|
||||
class="list-group-item list-group-item-action target-item"
|
||||
href={
|
||||
target.kind === TargetKind.Http
|
||||
? `/?warpgate-target=${target.name}`
|
||||
: '/@warpgate/admin'
|
||||
<ItemList load={loadTargets} showSearch={true}>
|
||||
<a
|
||||
slot="item" let:item={target}
|
||||
class="list-group-item list-group-item-action target-item"
|
||||
href={
|
||||
target.kind === TargetKind.WebAdmin
|
||||
? '/@warpgate/admin'
|
||||
: target.kind === TargetKind.Http
|
||||
? `/?warpgate-target=${target.name}`
|
||||
: '/@warpgate/admin'
|
||||
}
|
||||
on:click|preventDefault={e => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
on:click|preventDefault={e => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
selectTarget(target)
|
||||
}}
|
||||
>
|
||||
<span class="me-auto">
|
||||
selectTarget(target)
|
||||
}}
|
||||
>
|
||||
<span class="me-auto">
|
||||
{#if target.kind === TargetKind.WebAdmin}
|
||||
Manage Warpgate
|
||||
{:else}
|
||||
{target.name}
|
||||
</span>
|
||||
<small class="protocol text-muted ms-auto">
|
||||
{#if target.kind === TargetKind.Ssh}
|
||||
SSH
|
||||
{/if}
|
||||
{#if target.kind === TargetKind.MySql}
|
||||
MySQL
|
||||
{/if}
|
||||
</small>
|
||||
{#if target.kind === TargetKind.Http}
|
||||
<Fa icon={faArrowRight} fw />
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<DelayedSpinner />
|
||||
{/if}
|
||||
</span>
|
||||
<small class="protocol text-muted ms-auto">
|
||||
{#if target.kind === TargetKind.Ssh}
|
||||
SSH
|
||||
{/if}
|
||||
{#if target.kind === TargetKind.MySql}
|
||||
MySQL
|
||||
{/if}
|
||||
</small>
|
||||
{#if target.kind === TargetKind.Http || target.kind === TargetKind.WebAdmin}
|
||||
<Fa icon={faArrowRight} fw />
|
||||
{/if}
|
||||
</a>
|
||||
</ItemList>
|
||||
|
||||
<Modal isOpen={!!selectedTarget} toggle={() => selectedTarget = undefined}>
|
||||
<ModalHeader toggle={() => selectedTarget = undefined}>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Warpgate HTTP proxy",
|
||||
"version": "0.7.0"
|
||||
"version": "0.7.1"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -237,6 +237,18 @@
|
|||
},
|
||||
"/targets": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "search",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
|
|
Loading…
Reference in a new issue