mirror of
https://github.com/1f349/admin.1f349.com.git
synced 2024-11-13 23:21:32 +00:00
Separate routes and redirects tables
This commit is contained in:
parent
fcc6589043
commit
ca21c244fc
@ -1,16 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type {SvelteComponent} from "svelte";
|
||||
import GeneralView from "./views/GeneralView.svelte";
|
||||
import VioletView from "./views/VioletView.svelte";
|
||||
import OrchidView from "./views/OrchidView.svelte";
|
||||
import RoutesView from "./views/RoutesView.svelte";
|
||||
import RedirectsView from "./views/RedirectsView.svelte";
|
||||
import CertificatesView from "./views/CertificatesView.svelte";
|
||||
import {loginStore, parseJwt, type LoginStore} from "./stores/login";
|
||||
import {openLoginPopup} from "./utils/login-popup";
|
||||
import {domainOption} from "./stores/domain-option";
|
||||
|
||||
let sidebarOptions: Array<{name: string; view: typeof SvelteComponent<{}>}> = [
|
||||
{name: "General", view: GeneralView},
|
||||
{name: "Violet", view: VioletView},
|
||||
{name: "Orchid", view: OrchidView},
|
||||
{name: "Routes", view: RoutesView},
|
||||
{name: "Redirects", view: RedirectsView},
|
||||
{name: "Certificates", view: CertificatesView},
|
||||
];
|
||||
let sidebarSelection: {name: string; view: typeof SvelteComponent<{}>} = sidebarOptions[0];
|
||||
|
||||
@ -68,7 +70,7 @@
|
||||
</header>
|
||||
<main>
|
||||
{#if $loginStore == null}
|
||||
<div id="option-view">Please login to continue</div>
|
||||
<div id="login-view">Please login to continue</div>
|
||||
{:else}
|
||||
<div id="sidebar">
|
||||
{#each sidebarOptions as item (item.name)}
|
||||
@ -169,6 +171,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#login-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#option-view {
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
|
@ -4,38 +4,38 @@
|
||||
import Flags from "./Flags.svelte";
|
||||
import RedirectCode from "./RedirectCode.svelte";
|
||||
|
||||
export let redirect: CSPair<Redirect>;
|
||||
export let value: CSPair<Redirect>;
|
||||
|
||||
function resetRedirect(): any {
|
||||
redirect.client = JSON.parse(JSON.stringify(redirect.server));
|
||||
value.client = JSON.parse(JSON.stringify(value.server));
|
||||
}
|
||||
|
||||
const descCols = 50;
|
||||
</script>
|
||||
|
||||
{#if noCPair(redirect)}
|
||||
{#if noCPair(value)}
|
||||
<tr class="deleted">
|
||||
<td class="code-font"><a href="https://{redirect.server.src}" target="_blank">{redirect.server.src}</a></td>
|
||||
<td><input type="text" class="code-font" disabled bind:value={redirect.server.dst} size={Math.max(20, redirect.server.dst.length + 2)} /></td>
|
||||
<td><Flags value={redirect.server.flags} keys={redirectKeys} /></td>
|
||||
<td><RedirectCode bind:code={redirect.server.code} /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} disabled value={redirect.server.desc} /></td>
|
||||
<td class="code-font"><a href="https://{value.server.src}" target="_blank">{value.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><Flags value={value.server.flags} keys={redirectKeys} /></td>
|
||||
<td><RedirectCode bind:code={value.server.code} /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} disabled value={value.server.desc} /></td>
|
||||
<td><input type="checkbox" disabled checked={false} /></td>
|
||||
<td><button on:click={() => resetRedirect()}>Restore</button></td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr class:created={noSPair(redirect)} class:modified={!noSPair(redirect) && !redirectEqual(redirect.client, redirect.server)}>
|
||||
<td class="code-font"><a href="https://{redirect.client.src}" target="_blank">{redirect.client.src}</a></td>
|
||||
<td><input type="text" class="code-font" bind:value={redirect.client.dst} size={Math.max(20, redirect.client.dst.length + 2)} /></td>
|
||||
<td><Flags bind:value={redirect.client.flags} editable keys={redirectKeys} /></td>
|
||||
<td><RedirectCode bind:code={redirect.client.code} editable /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} bind:value={redirect.client.desc} /></td>
|
||||
<td><input type="checkbox" bind:checked={redirect.client.active} /></td>
|
||||
<tr class:created={noSPair(value)} class:modified={!noSPair(value) && !redirectEqual(value.client, value.server)}>
|
||||
<td class="code-font"><a href="https://{value.client.src}" target="_blank">{value.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><Flags bind:value={value.client.flags} editable keys={redirectKeys} /></td>
|
||||
<td><RedirectCode bind:code={value.client.code} editable /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} bind:value={value.client.desc} /></td>
|
||||
<td><input type="checkbox" bind:checked={value.client.active} /></td>
|
||||
<td>
|
||||
{#if !noSPair(redirect)}
|
||||
{#if !noSPair(value)}
|
||||
<button on:click={() => resetRedirect()}>Reset</button>
|
||||
{/if}
|
||||
<button on:click={() => (redirect.client = null)}>Delete</button>
|
||||
<button on:click={() => (value.client = null)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
@ -3,36 +3,36 @@
|
||||
import {type Route, routeKeys, routeEqual} from "../types/target";
|
||||
import Flags from "./Flags.svelte";
|
||||
|
||||
export let route: CSPair<Route>;
|
||||
export let value: CSPair<Route>;
|
||||
|
||||
function resetRoute(): any {
|
||||
route.client = JSON.parse(JSON.stringify(route.server));
|
||||
value.client = JSON.parse(JSON.stringify(value.server));
|
||||
}
|
||||
|
||||
const descCols = 50;
|
||||
</script>
|
||||
|
||||
{#if noCPair(route)}
|
||||
{#if noCPair(value)}
|
||||
<tr class="deleted">
|
||||
<td class="code-font"><a href="https://{route.server.src}" target="_blank">{route.server.src}</a></td>
|
||||
<td><input type="text" class="code-font" disabled bind:value={route.server.dst} size={Math.max(20, route.server.dst.length + 2)} /></td>
|
||||
<td><Flags value={route.server.flags} keys={routeKeys} /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} disabled value={route.server.desc} /></td>
|
||||
<td class="code-font"><a href="https://{value.server.src}" target="_blank">{value.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><Flags value={value.server.flags} keys={routeKeys} /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} disabled value={value.server.desc} /></td>
|
||||
<td><input type="checkbox" disabled checked={false} /></td>
|
||||
<td><button on:click={() => resetRoute()}>Restore</button></td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr class:created={noSPair(route)} class:modified={!noSPair(route) && !routeEqual(route.client, route.server)}>
|
||||
<td class="code-font"><a href="https://{route.client.src}" target="_blank">{route.client.src}</a></td>
|
||||
<td><input type="text" class="code-font" bind:value={route.client.dst} size={Math.max(20, route.client.dst.length + 2)} /></td>
|
||||
<td><Flags bind:value={route.client.flags} editable keys={routeKeys} /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} bind:value={route.client.desc} /></td>
|
||||
<td><input type="checkbox" bind:checked={route.client.active} /></td>
|
||||
<tr class:created={noSPair(value)} class:modified={!noSPair(value) && !routeEqual(value.client, value.server)}>
|
||||
<td class="code-font"><a href="https://{value.client.src}" target="_blank">{value.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><Flags bind:value={value.client.flags} editable keys={routeKeys} /></td>
|
||||
<td class="desc"><textarea rows="3" cols={descCols} bind:value={value.client.desc} /></td>
|
||||
<td><input type="checkbox" bind:checked={value.client.active} /></td>
|
||||
<td>
|
||||
{#if !noSPair(route)}
|
||||
{#if !noSPair(value)}
|
||||
<button on:click={() => resetRoute()}>Reset</button>
|
||||
{/if}
|
||||
<button on:click={() => (route.client = null)}>Delete</button>
|
||||
<button on:click={() => (value.client = null)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
@ -1,26 +1,16 @@
|
||||
<script lang="ts">
|
||||
import RedirectCreator from "../components/RedirectCreator.svelte";
|
||||
import RouteCreator from "../components/RouteCreator.svelte";
|
||||
import RedirectRow from "../components/RedirectRow.svelte";
|
||||
import RouteRow from "../components/RouteRow.svelte";
|
||||
import {getBearer, loginStore, parseJwt, type LoginStore} from "../stores/login";
|
||||
import {getBearer} from "../stores/login";
|
||||
import type {CSPair} from "../types/cspair";
|
||||
import {type Route, type Redirect, routeEqual, redirectEqual} from "../types/target";
|
||||
import {type Redirect, redirectEqual} from "../types/target";
|
||||
import {domainOption} from "../stores/domain-option";
|
||||
|
||||
const apiViolet = import.meta.env.VITE_API_VIOLET;
|
||||
|
||||
let routeData: {[key: string]: CSPair<Route>} = {};
|
||||
let redirectData: {[key: string]: CSPair<Redirect>} = {};
|
||||
|
||||
let routeSrcs: string[] = [];
|
||||
let redirectSrcs: string[] = [];
|
||||
|
||||
$: routeSrcs = Object.entries(routeData)
|
||||
.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));
|
||||
$: redirectSrcs = Object.entries(redirectData)
|
||||
.filter(x => x[1].client != null || x[1].server != null)
|
||||
.map(x => x[0])
|
||||
@ -36,32 +26,10 @@
|
||||
return p.endsWith(domain);
|
||||
}
|
||||
|
||||
let promiseForRoutes: Promise<void>;
|
||||
let promiseForRedirects: Promise<void>;
|
||||
|
||||
reloadRoutes();
|
||||
reloadRedirects();
|
||||
|
||||
function reloadRoutes() {
|
||||
promiseForRoutes = 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 routes = x as Route[];
|
||||
let y: {[key: string]: CSPair<Route>} = {};
|
||||
routes.forEach(x => {
|
||||
y[x.src] = {client: JSON.parse(JSON.stringify(x)), server: x};
|
||||
});
|
||||
routeData = y;
|
||||
res();
|
||||
})
|
||||
.catch(x => rej(x));
|
||||
});
|
||||
}
|
||||
|
||||
function reloadRedirects() {
|
||||
promiseForRedirects = new Promise<void>((res, rej) => {
|
||||
fetch(apiViolet + "/redirect", {headers: {Authorization: getBearer()}})
|
||||
@ -89,25 +57,6 @@
|
||||
}
|
||||
|
||||
function saveChanges() {
|
||||
let routePromises = routeSrcs
|
||||
.map(x => routeData[x])
|
||||
.filter(x => x.client != null || x.server != null)
|
||||
.filter(x => !routeEqual(x.client, x.server))
|
||||
.map((x: CSPair<Route>): Savable<CSPair<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.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);
|
||||
});
|
||||
});
|
||||
|
||||
let redirectPromises = redirectSrcs
|
||||
.map(x => redirectData[x])
|
||||
.filter(x => x.client != null || x.server != null)
|
||||
@ -127,9 +76,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(routePromises).then(_ => {
|
||||
reloadRoutes();
|
||||
});
|
||||
Promise.all(redirectPromises).then(_ => {
|
||||
reloadRedirects();
|
||||
});
|
||||
@ -138,67 +84,17 @@
|
||||
|
||||
<div class="wrapper">
|
||||
<div style="padding:8px;background-color:#bb7900;">
|
||||
Warning: This is currently still under development, however it DOES update the real server routes and redirects
|
||||
Warning: This is currently still under development, however it DOES update the real server redirects
|
||||
</div>
|
||||
|
||||
<div class="scrolling-area">
|
||||
{#await promiseForRoutes}
|
||||
<div class="text-padding">
|
||||
<h2>Routes</h2>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
{:then}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="6"><h2>Routes</h2></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Destination</th>
|
||||
<th>Flags</th>
|
||||
<th>Description</th>
|
||||
<th>Active</th>
|
||||
<th>Option</th>
|
||||
</tr>
|
||||
<RouteCreator
|
||||
on:make={e => {
|
||||
const x = e.detail;
|
||||
routeData[x.src] = {client: x, server: routeData[x.src]?.server};
|
||||
routeSrcs.push(x.src);
|
||||
routeSrcs = routeSrcs;
|
||||
}}
|
||||
/>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each routeSrcs as src (src)}
|
||||
{#if routeData[src]}
|
||||
<RouteRow bind:route={routeData[src]} />
|
||||
{:else}
|
||||
<tr><td colspan="5">Error loading row for {src}</td></tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:catch err}
|
||||
<div class="text-padding">
|
||||
<h2>Routes</h2>
|
||||
<div>Administrator... I hardly know her?</div>
|
||||
<div>{err}</div>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
{#await promiseForRedirects}
|
||||
<div class="text-padding">
|
||||
<h2>Redirects</h2>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
{:then}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="8"><h2>Redirects</h2></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Destination</th>
|
||||
@ -220,7 +116,7 @@
|
||||
<tbody>
|
||||
{#each redirectSrcs as src (src)}
|
||||
{#if redirectData[src]}
|
||||
<RedirectRow bind:redirect={redirectData[src]} />
|
||||
<RedirectRow bind:value={redirectData[src]} />
|
||||
{:else}
|
||||
<tr><td colspan="5">Error loading row for {src}</td></tr>
|
||||
{/if}
|
||||
@ -229,7 +125,6 @@
|
||||
</table>
|
||||
{:catch err}
|
||||
<div class="text-padding">
|
||||
<h2>Redirects</h2>
|
||||
<div>Administrator... I hardly know her?</div>
|
||||
<div>{err}</div>
|
||||
</div>
|
||||
@ -253,10 +148,6 @@
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 4px 8px #0003, 0 6px 20px #00000030;
|
||||
|
||||
th h2 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global(th),
|
195
src/views/RoutesView.svelte
Normal file
195
src/views/RoutesView.svelte
Normal file
@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
import RouteCreator from "../components/RouteCreator.svelte";
|
||||
import RouteRow from "../components/RouteRow.svelte";
|
||||
import {getBearer} from "../stores/login";
|
||||
import type {CSPair} from "../types/cspair";
|
||||
import {type Route, routeEqual} from "../types/target";
|
||||
import {domainOption} from "../stores/domain-option";
|
||||
|
||||
const apiViolet = import.meta.env.VITE_API_VIOLET;
|
||||
|
||||
let tableData: {[key: string]: CSPair<Route>} = {};
|
||||
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));
|
||||
|
||||
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 promiseForRoutes: Promise<void>;
|
||||
|
||||
reloadRoutes();
|
||||
|
||||
function reloadRoutes() {
|
||||
promiseForRoutes = 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 routes = x as Route[];
|
||||
let y: {[key: string]: CSPair<Route>} = {};
|
||||
routes.forEach(x => {
|
||||
y[x.src] = {client: JSON.parse(JSON.stringify(x)), server: x};
|
||||
});
|
||||
tableData = y;
|
||||
res();
|
||||
})
|
||||
.catch(x => rej(x));
|
||||
});
|
||||
}
|
||||
|
||||
interface Savable<T> {
|
||||
type: "del" | "ins";
|
||||
v: T;
|
||||
p?: Promise<void>;
|
||||
}
|
||||
|
||||
function saveChanges() {
|
||||
let routePromises = tableKeys
|
||||
.map(x => tableData[x])
|
||||
.filter(x => x.client != null || x.server != null)
|
||||
.filter(x => !routeEqual(x.client, x.server))
|
||||
.map((x: CSPair<Route>): Savable<CSPair<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.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);
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(routePromises).then(_ => {
|
||||
reloadRoutes();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<div style="padding:8px;background-color:#bb7900;">
|
||||
Warning: This is currently still under development, however it DOES update the real server routes
|
||||
</div>
|
||||
|
||||
<div class="scrolling-area">
|
||||
{#await promiseForRoutes}
|
||||
<div class="text-padding">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
{:then}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Destination</th>
|
||||
<th>Flags</th>
|
||||
<th>Description</th>
|
||||
<th>Active</th>
|
||||
<th>Option</th>
|
||||
</tr>
|
||||
<RouteCreator
|
||||
on:make={e => {
|
||||
const x = e.detail;
|
||||
tableData[x.src] = {client: x, server: tableData[x.src]?.server};
|
||||
tableKeys.push(x.src);
|
||||
tableKeys = tableKeys;
|
||||
}}
|
||||
/>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each tableKeys as src (src)}
|
||||
{#if tableData[src]}
|
||||
<RouteRow bind:value={tableData[src]} />
|
||||
{:else}
|
||||
<tr><td colspan="5">Error loading row for {src}</td></tr>
|
||||
{/if}
|
||||
{/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 on:click={() => saveChanges()}>Save Changes</button>
|
||||
</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;
|
||||
|
||||
button {
|
||||
background-color: #04aa6d;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
height: 50px;
|
||||
padding: 4px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user