mirror of
https://github.com/1f349/admin.1f349.com.git
synced 2024-11-09 22:32:57 +00:00
Merge branch 'generic-target-view'
This commit is contained in:
commit
74b2969bac
@ -1,43 +1,49 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {type CSPair, noCPair, noSPair} from "../types/cspair";
|
import type {Writable} from "svelte/store";
|
||||||
|
import {type CSPair, noCPair, noSPair, noPair, yesCPair} from "../types/cspair";
|
||||||
import {redirectKeys, redirectEqual, type Redirect} from "../types/target";
|
import {redirectKeys, redirectEqual, type Redirect} from "../types/target";
|
||||||
import Flags from "./Flags.svelte";
|
import Flags from "./Flags.svelte";
|
||||||
import RedirectCode from "./RedirectCode.svelte";
|
import RedirectCode from "./RedirectCode.svelte";
|
||||||
|
|
||||||
export let value: CSPair<Redirect>;
|
export let value: Writable<CSPair<Redirect>>;
|
||||||
|
|
||||||
|
let item: CSPair<Redirect>;
|
||||||
|
$: item = $value;
|
||||||
|
|
||||||
function resetRedirect(): any {
|
function resetRedirect(): any {
|
||||||
value.client = JSON.parse(JSON.stringify(value.server));
|
item.client = JSON.parse(JSON.stringify(item.server));
|
||||||
}
|
}
|
||||||
|
|
||||||
const descCols = 50;
|
const descCols = 50;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if noCPair(value)}
|
{#if noCPair(item)}
|
||||||
<tr class="deleted">
|
<tr class="deleted">
|
||||||
<td class="code-font"><a href="https://{value.server.src}" target="_blank">{value.server.src}</a></td>
|
<td class="code-font"><a href="https://{item.server.src}" target="_blank">{item.server.src}</a></td>
|
||||||
<td><input type="text" class="code-font" disabled bind:value={value.server.dst} size={Math.max(20, value.server.dst.length + 2)} /></td>
|
<td><input type="text" class="code-font" disabled bind:value={item.server.dst} size={Math.max(20, item.server.dst.length + 2)} /></td>
|
||||||
<td><Flags value={value.server.flags} keys={redirectKeys} /></td>
|
<td><Flags value={item.server.flags} keys={redirectKeys} /></td>
|
||||||
<td><RedirectCode bind:code={value.server.code} /></td>
|
<td><RedirectCode bind:code={item.server.code} /></td>
|
||||||
<td class="desc"><textarea rows="3" cols={descCols} disabled value={value.server.desc} /></td>
|
<td class="desc"><textarea rows="3" cols={descCols} disabled value={item.server.desc} /></td>
|
||||||
<td><input type="checkbox" disabled checked={false} /></td>
|
<td><input type="checkbox" disabled checked={false} /></td>
|
||||||
<td><button on:click={() => resetRedirect()}>Restore</button></td>
|
<td><button on:click={() => resetRedirect()}>Restore</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else if yesCPair(item)}
|
||||||
<tr class:created={noSPair(value)} class:modified={!noSPair(value) && !redirectEqual(value.client, value.server)}>
|
<tr class:created={noSPair(item)} class:modified={!noSPair(item) && !redirectEqual(item.client, item.server)}>
|
||||||
<td class="code-font"><a href="https://{value.client.src}" target="_blank">{value.client.src}</a></td>
|
<td class="code-font"><a href="https://{item.client.src}" target="_blank">{item.client.src}</a></td>
|
||||||
<td><input type="text" class="code-font" bind:value={value.client.dst} size={Math.max(20, value.client.dst.length + 2)} /></td>
|
<td><input type="text" class="code-font" bind:value={item.client.dst} size={Math.max(20, item.client.dst.length + 2)} /></td>
|
||||||
<td><Flags bind:value={value.client.flags} editable keys={redirectKeys} /></td>
|
<td><Flags bind:value={item.client.flags} editable keys={redirectKeys} /></td>
|
||||||
<td><RedirectCode bind:code={value.client.code} editable /></td>
|
<td><RedirectCode bind:code={item.client.code} editable /></td>
|
||||||
<td class="desc"><textarea rows="3" cols={descCols} bind:value={value.client.desc} /></td>
|
<td class="desc"><textarea rows="3" cols={descCols} bind:value={item.client.desc} /></td>
|
||||||
<td><input type="checkbox" bind:checked={value.client.active} /></td>
|
<td><input type="checkbox" bind:checked={item.client.active} /></td>
|
||||||
<td>
|
<td>
|
||||||
{#if !noSPair(value)}
|
{#if !noSPair(item)}
|
||||||
<button on:click={() => resetRedirect()}>Reset</button>
|
<button on:click={() => resetRedirect()}>Reset</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button on:click={() => (value.client = null)}>Delete</button>
|
<button on:click={() => (item.client = null)}>Delete</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<div>Invalid redirect row: please report this error</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -1,40 +1,46 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {type CSPair, noCPair, noSPair} from "../types/cspair";
|
import type {Writable} from "svelte/store";
|
||||||
|
import {type CSPair, noCPair, noSPair, yesCPair} from "../types/cspair";
|
||||||
import {type Route, routeKeys, routeEqual} from "../types/target";
|
import {type Route, routeKeys, routeEqual} from "../types/target";
|
||||||
import Flags from "./Flags.svelte";
|
import Flags from "./Flags.svelte";
|
||||||
|
|
||||||
export let value: CSPair<Route>;
|
export let value: Writable<CSPair<Route>>;
|
||||||
|
|
||||||
|
let item: CSPair<Route>;
|
||||||
|
$: item = $value;
|
||||||
|
|
||||||
function resetRoute(): any {
|
function resetRoute(): any {
|
||||||
value.client = JSON.parse(JSON.stringify(value.server));
|
item.client = JSON.parse(JSON.stringify(item.server));
|
||||||
}
|
}
|
||||||
|
|
||||||
const descCols = 50;
|
const descCols = 50;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if noCPair(value)}
|
{#if noCPair(item)}
|
||||||
<tr class="deleted">
|
<tr class="deleted">
|
||||||
<td class="code-font"><a href="https://{value.server.src}" target="_blank">{value.server.src}</a></td>
|
<td class="code-font"><a href="https://{item.server.src}" target="_blank">{item.server.src}</a></td>
|
||||||
<td><input type="text" class="code-font" disabled bind:value={value.server.dst} size={Math.max(20, value.server.dst.length + 2)} /></td>
|
<td><input type="text" class="code-font" disabled bind:value={item.server.dst} size={Math.max(20, item.server.dst.length + 2)} /></td>
|
||||||
<td><Flags value={value.server.flags} keys={routeKeys} /></td>
|
<td><Flags value={item.server.flags} keys={routeKeys} /></td>
|
||||||
<td class="desc"><textarea rows="3" cols={descCols} disabled value={value.server.desc} /></td>
|
<td class="desc"><textarea rows="3" cols={descCols} disabled value={item.server.desc} /></td>
|
||||||
<td><input type="checkbox" disabled checked={false} /></td>
|
<td><input type="checkbox" disabled checked={false} /></td>
|
||||||
<td><button on:click={() => resetRoute()}>Restore</button></td>
|
<td><button on:click={() => resetRoute()}>Restore</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else if yesCPair(item)}
|
||||||
<tr class:created={noSPair(value)} class:modified={!noSPair(value) && !routeEqual(value.client, value.server)}>
|
<tr class:created={noSPair(item)} class:modified={!noSPair(item) && !routeEqual(item.client, item.server)}>
|
||||||
<td class="code-font"><a href="https://{value.client.src}" target="_blank">{value.client.src}</a></td>
|
<td class="code-font"><a href="https://{item.client.src}" target="_blank">{item.client.src}</a></td>
|
||||||
<td><input type="text" class="code-font" bind:value={value.client.dst} size={Math.max(20, value.client.dst.length + 2)} /></td>
|
<td><input type="text" class="code-font" bind:value={item.client.dst} size={Math.max(20, item.client.dst.length + 2)} /></td>
|
||||||
<td><Flags bind:value={value.client.flags} editable keys={routeKeys} /></td>
|
<td><Flags bind:value={item.client.flags} editable keys={routeKeys} /></td>
|
||||||
<td class="desc"><textarea rows="3" cols={descCols} bind:value={value.client.desc} /></td>
|
<td class="desc"><textarea rows="3" cols={descCols} bind:value={item.client.desc} /></td>
|
||||||
<td><input type="checkbox" bind:checked={value.client.active} /></td>
|
<td><input type="checkbox" bind:checked={item.client.active} /></td>
|
||||||
<td>
|
<td>
|
||||||
{#if !noSPair(value)}
|
{#if !noSPair(item)}
|
||||||
<button on:click={() => resetRoute()}>Reset</button>
|
<button on:click={() => resetRoute()}>Reset</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button on:click={() => (value.client = null)}>Delete</button>
|
<button on:click={() => (item.client = null)}>Delete</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<div>Invalid redirect row: please report this error</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
export type CSPair<T> = {client: T; server: T; p: Promise<void>} | CSPairNotC<T> | CSPairNotS<T>;
|
export type CSPair<T> = CSPairYesC<T> | CSPairYesS<T>;
|
||||||
export type CSPairNotC<T> = {client: null; server: T; p: Promise<void>};
|
export type CSPairNotC<T> = {client: null; server: T; p: Promise<void>};
|
||||||
export type CSPairNotS<T> = {client: T; server: null; p: Promise<void>};
|
export type CSPairNotS<T> = {client: T; server: null; p: Promise<void>};
|
||||||
|
export type CSPairYesC<T> = {client: T; server: T | null; p: Promise<void>};
|
||||||
|
export type CSPairYesS<T> = {client: T | null; server: T; p: Promise<void>};
|
||||||
|
|
||||||
|
export function noPair<T>(pair: CSPair<T>): boolean {
|
||||||
|
return pair.client == null || pair.server == null;
|
||||||
|
}
|
||||||
|
|
||||||
export function noCPair<T>(pair: CSPair<T>): pair is CSPairNotC<T> {
|
export function noCPair<T>(pair: CSPair<T>): pair is CSPairNotC<T> {
|
||||||
return pair.client == null;
|
return pair.client == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function yesCPair<T>(pair: CSPair<T>): pair is CSPairYesC<T> {
|
||||||
|
return pair.client != null;
|
||||||
|
}
|
||||||
|
|
||||||
export function noSPair<T>(pair: CSPair<T>): pair is CSPairNotS<T> {
|
export function noSPair<T>(pair: CSPair<T>): pair is CSPairNotS<T> {
|
||||||
return pair.server == null;
|
return pair.server == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function yesSPair<T>(pair: CSPair<T>): pair is CSPairYesS<T> {
|
||||||
|
return pair.server != null;
|
||||||
|
}
|
||||||
|
@ -1,112 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import RedirectCreator from "../components/RedirectCreator.svelte";
|
import RedirectCreator from "../components/RedirectCreator.svelte";
|
||||||
import RedirectRow from "../components/RedirectRow.svelte";
|
import RedirectRow from "../components/RedirectRow.svelte";
|
||||||
import {getBearer} from "../stores/login";
|
import {redirectEqual} from "../types/target";
|
||||||
import type {CSPair} from "../types/cspair";
|
import {redirectsTable} from "../stores/target";
|
||||||
import {type Redirect, redirectEqual} from "../types/target";
|
import TargetManagementView from "./TargetManagementView.svelte";
|
||||||
import {domainOption} from "../stores/domain-option";
|
|
||||||
import {redirectsTable, type CountStats, tableCountStats} from "../stores/target";
|
|
||||||
|
|
||||||
const apiViolet = import.meta.env.VITE_API_VIOLET;
|
const apiViolet = import.meta.env.VITE_API_VIOLET;
|
||||||
|
|
||||||
let tableKeys: string[] = [];
|
|
||||||
|
|
||||||
$: tableKeys = Object.entries($redirectsTable)
|
|
||||||
.filter(x => x[1].client != null || x[1].server != null)
|
|
||||||
.map(x => x[0])
|
|
||||||
.filter(x => domainFilter(x, $domainOption))
|
|
||||||
.sort((a, b) => a.localeCompare(b));
|
|
||||||
|
|
||||||
let rowStats: CountStats = {created: 0, modified: 0, removed: 0};
|
|
||||||
|
|
||||||
$: rowStats = tableCountStats($redirectsTable, tableKeys, redirectEqual);
|
|
||||||
|
|
||||||
function domainFilter(src: string, domain: string) {
|
|
||||||
if (domain == "*") return true;
|
|
||||||
let n = src.indexOf("/");
|
|
||||||
if (n == -1) n = src.length;
|
|
||||||
let p = src.slice(0, n);
|
|
||||||
if (p == domain) return true;
|
|
||||||
return p.endsWith(domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
let promiseForTable: Promise<void> = Object.keys($redirectsTable).length === 0 ? reloadTable() : Promise.resolve();
|
|
||||||
|
|
||||||
function reloadTable(): Promise<void> {
|
|
||||||
return new Promise<void>((res, rej) => {
|
|
||||||
fetch(apiViolet + "/redirect", {headers: {Authorization: getBearer()}})
|
|
||||||
.then(x => {
|
|
||||||
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
|
|
||||||
return x.json();
|
|
||||||
})
|
|
||||||
.then(x => {
|
|
||||||
let rows = x as Redirect[];
|
|
||||||
let srcs = new Set(Object.keys($redirectsTable));
|
|
||||||
rows.forEach(x => {
|
|
||||||
$redirectsTable[x.src] = {
|
|
||||||
client: !$redirectsTable[x.src] ? JSON.parse(JSON.stringify(x)) : $redirectsTable[x.src]?.client,
|
|
||||||
server: x,
|
|
||||||
p: Promise.resolve(),
|
|
||||||
};
|
|
||||||
srcs.delete(x.src);
|
|
||||||
});
|
|
||||||
srcs.forEach(x => {
|
|
||||||
$redirectsTable[x].server = null;
|
|
||||||
});
|
|
||||||
res();
|
|
||||||
})
|
|
||||||
.catch(x => rej(x));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Savable<T> {
|
|
||||||
type: "del" | "ins";
|
|
||||||
v: CSPair<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveChanges() {
|
|
||||||
let tableProm = tableKeys
|
|
||||||
.map(x => $redirectsTable[x])
|
|
||||||
.filter(x => x.client != null || x.server != null)
|
|
||||||
.filter(x => !redirectEqual(x.client, x.server))
|
|
||||||
.map((x: CSPair<Redirect>): Savable<Redirect> => {
|
|
||||||
if (x.client == null && x.server != null) return {type: "del", v: x};
|
|
||||||
return {type: "ins", v: x};
|
|
||||||
})
|
|
||||||
.sort((a, _) => (a.type === "del" ? -1 : a.type === "ins" ? 1 : 0))
|
|
||||||
.map(x => {
|
|
||||||
x.v.p = fetch(apiViolet + "/redirect", {
|
|
||||||
method: x.type == "del" ? "DELETE" : "POST",
|
|
||||||
headers: {Authorization: getBearer()},
|
|
||||||
body: JSON.stringify(x.type == "del" ? {src: (x.v.server as Redirect).src} : x.v.client),
|
|
||||||
}).then(x => {
|
|
||||||
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
|
|
||||||
});
|
|
||||||
return x.v.p;
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(tableProm)
|
|
||||||
.then(_ => reloadTable())
|
|
||||||
.catch(_ => {
|
|
||||||
alert("Some rows failed to save changes");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper">
|
<TargetManagementView apiUrl="{apiViolet}/redirect" tableData={redirectsTable} equality={redirectEqual}>
|
||||||
<div style="padding:8px;background-color:#bb7900;">
|
<svelte:fragment slot="headers">
|
||||||
Warning: This is currently still under development, however it DOES update the real server redirects
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scrolling-area">
|
|
||||||
{#await promiseForTable}
|
|
||||||
<div class="text-padding">
|
|
||||||
<div>Loading...</div>
|
|
||||||
</div>
|
|
||||||
{:then}
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th>Destination</th>
|
<th>Destination</th>
|
||||||
<th>Flags</th>
|
<th>Flags</th>
|
||||||
@ -114,98 +17,7 @@
|
|||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Active</th>
|
<th>Active</th>
|
||||||
<th>Option</th>
|
<th>Option</th>
|
||||||
</tr>
|
</svelte:fragment>
|
||||||
<RedirectCreator
|
<RedirectRow slot="row" let:value {value} />
|
||||||
on:make={e => {
|
<RedirectCreator slot="creator" let:make on:make={e => make(e)} />
|
||||||
const x = e.detail;
|
</TargetManagementView>
|
||||||
$redirectsTable[x.src] = {client: x, server: $redirectsTable[x.src]?.server, p: Promise.resolve()};
|
|
||||||
tableKeys.push(x.src);
|
|
||||||
tableKeys = tableKeys;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each tableKeys as src (src)}
|
|
||||||
{#await $redirectsTable[src].p}
|
|
||||||
<tr><td colspan="5">Loading...</td></tr>
|
|
||||||
{:then _}
|
|
||||||
<RedirectRow bind:value={$redirectsTable[src]} />
|
|
||||||
{:catch err}
|
|
||||||
<tr><td colspan="5">Error loading row for {src}: {err}</td></tr>
|
|
||||||
{/await}
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{:catch err}
|
|
||||||
<div class="text-padding">
|
|
||||||
<div>Administrator... I hardly know her?</div>
|
|
||||||
<div>{err}</div>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<button class="btn-green" on:click={() => saveChanges()}>Save Changes</button>
|
|
||||||
{#if rowStats.created > 0}
|
|
||||||
<div class="meta-info">{rowStats.created} new redirect{rowStats.created > 1 ? "s" : ""}</div>
|
|
||||||
{/if}
|
|
||||||
{#if rowStats.modified > 0}
|
|
||||||
<div class="meta-info">{rowStats.modified} unsaved change{rowStats.modified > 1 ? "s" : ""}</div>
|
|
||||||
{/if}
|
|
||||||
{#if rowStats.removed > 0}
|
|
||||||
<div class="meta-info">{rowStats.removed} removed redirect{rowStats.removed > 1 ? "s" : ""}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
thead {
|
|
||||||
background-color: #333333;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
box-shadow: 0 4px 8px #0003, 0 6px 20px #00000030;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(th),
|
|
||||||
:global(td) {
|
|
||||||
padding: 11px 8px 11px 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.scrolling-area {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-padding {
|
|
||||||
padding: 4px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
height: 50px;
|
|
||||||
background-color: #2c2c2c;
|
|
||||||
box-shadow: 0 -4px 8px #0003, 0 -6px 20px #00000030;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.meta-info {
|
|
||||||
line-height: 50px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,210 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import RouteCreator from "../components/RouteCreator.svelte";
|
import RouteCreator from "../components/RouteCreator.svelte";
|
||||||
import RouteRow from "../components/RouteRow.svelte";
|
import RouteRow from "../components/RouteRow.svelte";
|
||||||
import {getBearer} from "../stores/login";
|
import {routeEqual} from "../types/target";
|
||||||
import type {CSPair} from "../types/cspair";
|
import {routesTable} from "../stores/target";
|
||||||
import {type Route, routeEqual} from "../types/target";
|
import TargetManagementView from "./TargetManagementView.svelte";
|
||||||
import {domainOption} from "../stores/domain-option";
|
|
||||||
import {routesTable, type CountStats, tableCountStats} from "../stores/target";
|
|
||||||
|
|
||||||
const apiViolet = import.meta.env.VITE_API_VIOLET;
|
const apiViolet = import.meta.env.VITE_API_VIOLET;
|
||||||
|
|
||||||
let tableKeys: string[] = [];
|
|
||||||
|
|
||||||
$: tableKeys = Object.entries($routesTable)
|
|
||||||
.filter(x => x[1].client != null || x[1].server != null)
|
|
||||||
.map(x => x[0])
|
|
||||||
.filter(x => domainFilter(x, $domainOption))
|
|
||||||
.sort((a, b) => a.localeCompare(b));
|
|
||||||
|
|
||||||
let rowStats: CountStats = {created: 0, modified: 0, removed: 0};
|
|
||||||
|
|
||||||
$: rowStats = tableCountStats($routesTable, tableKeys, routeEqual);
|
|
||||||
|
|
||||||
function domainFilter(src: string, domain: string) {
|
|
||||||
if (domain == "*") return true;
|
|
||||||
let n = src.indexOf("/");
|
|
||||||
if (n == -1) n = src.length;
|
|
||||||
let p = src.slice(0, n);
|
|
||||||
if (p == domain) return true;
|
|
||||||
return p.endsWith(domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
let promiseForTable: Promise<void> = Object.keys($routesTable).length === 0 ? reloadTable() : Promise.resolve();
|
|
||||||
|
|
||||||
function reloadTable(): Promise<void> {
|
|
||||||
return new Promise<void>((res, rej) => {
|
|
||||||
fetch(apiViolet + "/route", {headers: {Authorization: getBearer()}})
|
|
||||||
.then(x => {
|
|
||||||
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
|
|
||||||
return x.json();
|
|
||||||
})
|
|
||||||
.then(x => {
|
|
||||||
let rows = x as Route[];
|
|
||||||
let srcs = new Set(Object.keys($routesTable));
|
|
||||||
rows.forEach(x => {
|
|
||||||
$routesTable[x.src] = {
|
|
||||||
client: !$routesTable[x.src] ? JSON.parse(JSON.stringify(x)) : $routesTable[x.src]?.client,
|
|
||||||
server: x,
|
|
||||||
p: Promise.resolve(),
|
|
||||||
};
|
|
||||||
srcs.delete(x.src);
|
|
||||||
});
|
|
||||||
srcs.forEach(x => {
|
|
||||||
$routesTable[x].server = null;
|
|
||||||
});
|
|
||||||
res();
|
|
||||||
})
|
|
||||||
.catch(x => rej(x));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Savable<T> {
|
|
||||||
type: "del" | "ins";
|
|
||||||
v: CSPair<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveChanges() {
|
|
||||||
let tableProm = tableKeys
|
|
||||||
.map(x => $routesTable[x])
|
|
||||||
.filter(x => x.client != null || x.server != null)
|
|
||||||
.filter(x => !routeEqual(x.client, x.server))
|
|
||||||
.map((x: CSPair<Route>): Savable<Route> => {
|
|
||||||
if (x.client == null && x.server != null) return {type: "del", v: x};
|
|
||||||
return {type: "ins", v: x};
|
|
||||||
})
|
|
||||||
.sort((a, _) => (a.type === "del" ? -1 : a.type === "ins" ? 1 : 0))
|
|
||||||
.map(x => {
|
|
||||||
x.v.p = fetch(apiViolet + "/route", {
|
|
||||||
method: x.type == "del" ? "DELETE" : "POST",
|
|
||||||
headers: {Authorization: getBearer()},
|
|
||||||
body: JSON.stringify(x.type == "del" ? {src: (x.v.server as Route).src} : x.v.client),
|
|
||||||
}).then(x => {
|
|
||||||
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
|
|
||||||
});
|
|
||||||
return x.v.p;
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(tableProm)
|
|
||||||
.then(_ => reloadTable())
|
|
||||||
.catch(_ => {
|
|
||||||
alert("Some rows failed to save changes");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper">
|
<TargetManagementView apiUrl="{apiViolet}/route" tableData={routesTable} equality={routeEqual}>
|
||||||
<div style="padding:8px;background-color:#bb7900;">
|
<svelte:fragment slot="headers">
|
||||||
Warning: This is currently still under development, however it DOES update the real server routes
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scrolling-area">
|
|
||||||
{#await promiseForTable}
|
|
||||||
<div class="text-padding">
|
|
||||||
<div>Loading...</div>
|
|
||||||
</div>
|
|
||||||
{:then}
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th>Destination</th>
|
<th>Destination</th>
|
||||||
<th>Flags</th>
|
<th>Flags</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Active</th>
|
<th>Active</th>
|
||||||
<th>Option</th>
|
<th>Option</th>
|
||||||
</tr>
|
</svelte:fragment>
|
||||||
<RouteCreator
|
<RouteRow slot="row" let:value {value} />
|
||||||
on:make={e => {
|
<RouteCreator slot="creator" let:make on:make={e => make(e)} />
|
||||||
const x = e.detail;
|
</TargetManagementView>
|
||||||
$routesTable[x.src] = {client: x, server: $routesTable[x.src]?.server, p: Promise.resolve()};
|
|
||||||
tableKeys.push(x.src);
|
|
||||||
tableKeys = tableKeys;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each tableKeys as src (src)}
|
|
||||||
{#await $routesTable[src].p}
|
|
||||||
<tr><td colspan="5">Loading...</td></tr>
|
|
||||||
{:then _}
|
|
||||||
<RouteRow bind:value={$routesTable[src]} />
|
|
||||||
{:catch err}
|
|
||||||
<tr><td colspan="5">Error loading row for {src}: {err}</td></tr>
|
|
||||||
{/await}
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{:catch err}
|
|
||||||
<div class="text-padding">
|
|
||||||
<div>Administrator... I hardly know her?</div>
|
|
||||||
<div>{err}</div>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<button class="btn-green" on:click={() => saveChanges()}>Save Changes</button>
|
|
||||||
{#if rowStats.created > 0}
|
|
||||||
<div class="meta-info">{rowStats.created} new route{rowStats.created > 1 ? "s" : ""}</div>
|
|
||||||
{/if}
|
|
||||||
{#if rowStats.modified > 0}
|
|
||||||
<div class="meta-info">{rowStats.modified} unsaved change{rowStats.modified > 1 ? "s" : ""}</div>
|
|
||||||
{/if}
|
|
||||||
{#if rowStats.removed > 0}
|
|
||||||
<div class="meta-info">{rowStats.removed} removed route{rowStats.removed > 1 ? "s" : ""}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
thead {
|
|
||||||
background-color: #333333;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
box-shadow: 0 4px 8px #0003, 0 6px 20px #00000030;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(th),
|
|
||||||
:global(td) {
|
|
||||||
padding: 11px 8px 11px 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.scrolling-area {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-padding {
|
|
||||||
padding: 4px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
height: 50px;
|
|
||||||
background-color: #2c2c2c;
|
|
||||||
box-shadow: 0 -4px 8px #0003, 0 -6px 20px #00000030;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.meta-info {
|
|
||||||
line-height: 50px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
214
src/views/TargetManagementView.svelte
Normal file
214
src/views/TargetManagementView.svelte
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
interface Target {
|
||||||
|
src: string;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {writable, type Writable} from "svelte/store";
|
||||||
|
|
||||||
|
import {getBearer} from "../stores/login";
|
||||||
|
import type {CSPair} from "../types/cspair";
|
||||||
|
import {domainOption} from "../stores/domain-option";
|
||||||
|
import {type CountStats, tableCountStats} from "../stores/target";
|
||||||
|
|
||||||
|
type T = $$Generic<Target>;
|
||||||
|
|
||||||
|
export let apiUrl: string;
|
||||||
|
export let tableData: Writable<{[key: string]: CSPair<T>}>;
|
||||||
|
export let equality: (a: T | null, b: T | null) => boolean;
|
||||||
|
|
||||||
|
let tableKeys: string[] = [];
|
||||||
|
|
||||||
|
$: tableKeys = Object.entries($tableData)
|
||||||
|
.filter(x => x[1].client != null || x[1].server != null)
|
||||||
|
.map(x => x[0])
|
||||||
|
.filter(x => domainFilter(x, $domainOption))
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
let rowStats: CountStats = {created: 0, modified: 0, removed: 0};
|
||||||
|
|
||||||
|
$: rowStats = tableCountStats($tableData, tableKeys, equality);
|
||||||
|
|
||||||
|
function domainFilter(src: string, domain: string) {
|
||||||
|
if (domain == "*") return true;
|
||||||
|
let n = src.indexOf("/");
|
||||||
|
if (n == -1) n = src.length;
|
||||||
|
let p = src.slice(0, n);
|
||||||
|
if (p == domain) return true;
|
||||||
|
return p.endsWith(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
let promiseForTable: Promise<void> = Object.keys($tableData).length === 0 ? reloadTable() : Promise.resolve();
|
||||||
|
|
||||||
|
function reloadTable(): Promise<void> {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
fetch(apiUrl, {headers: {Authorization: getBearer()}})
|
||||||
|
.then(x => {
|
||||||
|
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
|
||||||
|
return x.json();
|
||||||
|
})
|
||||||
|
.then(x => {
|
||||||
|
let rows = x as T[];
|
||||||
|
let srcs = new Set(Object.keys($tableData));
|
||||||
|
rows.forEach(x => {
|
||||||
|
$tableData[x.src] = {
|
||||||
|
client: !$tableData[x.src] ? JSON.parse(JSON.stringify(x)) : $tableData[x.src]?.client,
|
||||||
|
server: x,
|
||||||
|
p: Promise.resolve(),
|
||||||
|
};
|
||||||
|
srcs.delete(x.src);
|
||||||
|
});
|
||||||
|
srcs.forEach(x => {
|
||||||
|
$tableData[x].server = null;
|
||||||
|
});
|
||||||
|
res();
|
||||||
|
})
|
||||||
|
.catch(x => rej(x));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Savable<T> {
|
||||||
|
type: "del" | "ins";
|
||||||
|
v: CSPair<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChanges() {
|
||||||
|
let tableProm = tableKeys
|
||||||
|
.map(x => $tableData[x])
|
||||||
|
.filter(x => x.client != null || x.server != null)
|
||||||
|
.filter(x => !equality(x.client, x.server))
|
||||||
|
.map((x: CSPair<T>): Savable<T> => {
|
||||||
|
if (x.client == null && x.server != null) return {type: "del", v: x};
|
||||||
|
return {type: "ins", v: x};
|
||||||
|
})
|
||||||
|
.sort((a, _) => (a.type === "del" ? -1 : a.type === "ins" ? 1 : 0))
|
||||||
|
.map(x => {
|
||||||
|
x.v.p = fetch(apiUrl, {
|
||||||
|
method: x.type == "del" ? "DELETE" : "POST",
|
||||||
|
headers: {Authorization: getBearer()},
|
||||||
|
body: JSON.stringify(x.type == "del" ? {src: (x.v.server as T).src} : x.v.client),
|
||||||
|
}).then(x => {
|
||||||
|
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
|
||||||
|
});
|
||||||
|
return x.v.p;
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(tableProm)
|
||||||
|
.then(_ => reloadTable())
|
||||||
|
.catch(_ => {
|
||||||
|
alert("Some rows failed to save changes");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function make(e: CustomEvent<T>) {
|
||||||
|
const x = e.detail as unknown as T;
|
||||||
|
$tableData[x.src] = {client: x, server: $tableData[x.src]?.server, p: Promise.resolve()};
|
||||||
|
tableKeys.push(x.src);
|
||||||
|
tableKeys = tableKeys;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<div style="padding:8px;background-color:#bb7900;">
|
||||||
|
Warning: This is currently still under development, however it DOES send updates to the real server
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scrolling-area">
|
||||||
|
{#await promiseForTable}
|
||||||
|
<div class="text-padding">
|
||||||
|
<div>Loading...</div>
|
||||||
|
</div>
|
||||||
|
{:then}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<slot name="headers" />
|
||||||
|
</tr>
|
||||||
|
<slot name="creator" {make} />
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each tableKeys as src (src)}
|
||||||
|
{#await $tableData[src].p}
|
||||||
|
<tr><td colspan="5">Loading...</td></tr>
|
||||||
|
{:then _}
|
||||||
|
<slot name="row" value={writable($tableData[src])} />
|
||||||
|
{:catch err}
|
||||||
|
<tr><td colspan="5">Error loading row for {src}: {err}</td></tr>
|
||||||
|
{/await}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{:catch err}
|
||||||
|
<div class="text-padding">
|
||||||
|
<div>Administrator... I hardly know her?</div>
|
||||||
|
<div>{err}</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<button class="btn-green" on:click={() => saveChanges()}>Save Changes</button>
|
||||||
|
{#if rowStats.created > 0}
|
||||||
|
<div class="meta-info">{rowStats.created} new row{rowStats.created > 1 ? "s" : ""}</div>
|
||||||
|
{/if}
|
||||||
|
{#if rowStats.modified > 0}
|
||||||
|
<div class="meta-info">{rowStats.modified} unsaved change{rowStats.modified > 1 ? "s" : ""}</div>
|
||||||
|
{/if}
|
||||||
|
{#if rowStats.removed > 0}
|
||||||
|
<div class="meta-info">{rowStats.removed} removed row{rowStats.removed > 1 ? "s" : ""}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background-color: #333333;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 4px 8px #0003, 0 6px 20px #00000030;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(th),
|
||||||
|
:global(td) {
|
||||||
|
padding: 11px 8px 11px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.scrolling-area {
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-padding {
|
||||||
|
padding: 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
height: 50px;
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
box-shadow: 0 -4px 8px #0003, 0 -6px 20px #00000030;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
line-height: 50px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user