ui: Huge modals refactor

This commit is contained in:
nemunaire 2023-11-23 15:21:40 +01:00
parent f70943cff3
commit 5939df0455
16 changed files with 307 additions and 217 deletions

View File

@ -0,0 +1,30 @@
<script context="module" lang="ts">
export const controls = { };
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import ServiceSelectorModal, { controls as ctrlServiceSelector } from '$lib/components/domains/ServiceSelectorModal.svelte';
import { controls as ctrlService } from '$lib/components/domains/ServiceModal.svelte';
import type { Domain, DomainInList } from '$lib/model/domain';
import type { ServiceCombined } from '$lib/model/service';
import type { Zone } from '$lib/model/zone';
const dispatch = createEventDispatcher();
export let origin: DomainInList | Domain;
export let zone: Zone;
function Open(domain: string): void {
ctrlServiceSelector.Open(domain);
}
controls.Open = Open;
</script>
<ServiceSelectorModal
{origin}
zservices={zone.services}
on:show-next-modal={(event) => ctrlService.Open(event.detail)}
/>

View File

@ -0,0 +1,26 @@
<script context="module" lang="ts">
export const controls = { };
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import NewSubdomainModal, { controls as ctrlNewSubdomainModal } from '$lib/components/domains/NewSubdomainModal.svelte';
import { controls as ctrlServicePath } from '$lib/components/NewServicePath.svelte';
import type { Domain, DomainInList } from '$lib/model/domain';
import type { ServiceCombined } from '$lib/model/service';
import type { Zone } from '$lib/model/zone';
const dispatch = createEventDispatcher();
export let origin: DomainInList | Domain;
controls.Open = () => {
ctrlNewSubdomainModal.Open();
}
</script>
<NewSubdomainModal
{origin}
on:show-next-modal={(event) => ctrlServicePath.Open(event.detail)}
/>

View File

@ -1,3 +1,7 @@
<script context="module" lang="ts">
export const controls = { };
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
@ -93,6 +97,13 @@
);
}
}
function Open(domain: string): void {
dn = domain;
isOpen = true;
}
controls.Open = Open;
</script>
<Modal

View File

@ -1,3 +1,7 @@
<script context="module" lang="ts">
export const controls = { };
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
@ -22,8 +26,6 @@
export let isOpen = false;
const toggle = () => (isOpen = !isOpen);
$: if (isOpen) value = "";
export let origin: Domain | DomainInList;
export let value: string = "";
@ -61,6 +63,13 @@
dispatch("show-next-modal", value);
}
}
function Open(domain): void {
isOpen = true;
value = '';
}
controls.Open = Open;
</script>
<Modal

View File

@ -1,3 +1,7 @@
<script context="module" lang="ts">
export const controls = { };
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
@ -23,9 +27,10 @@
const toggle = () => (isOpen = !isOpen);
export let origin: Domain | DomainInList;
export let service: ServiceCombined;
export let zone: Zone;
let service: ServiceCombined | undefined = undefined;
let addServiceInProgress = false;
let deleteServiceInProgress = false;
@ -64,8 +69,16 @@
}
);
}
function Open(svc: ServiceCombined): void {
service = svc;
isOpen = true;
}
controls.Open = Open;
</script>
{#if service && service._domain !== undefined}
<Modal
{isOpen}
{toggle}
@ -108,3 +121,4 @@
on:delete-service={deleteService}
/>
</Modal>
{/if}

View File

@ -1,3 +1,7 @@
<script context="module" lang="ts">
export const controls = { };
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
@ -18,12 +22,6 @@
export let isOpen = false;
const toggle = () => (isOpen = !isOpen);
$: {
if (isOpen) {
value = "";
}
}
export let dn: string;
export let origin: Domain | DomainInList;
export let value: string | null = null;
@ -35,6 +33,14 @@
dispatch("show-next-modal", {_svctype: value, _domain: dn, Service: { }});
}
}
function Open(domain: string): void {
dn = domain;
isOpen = true;
value = '';
}
controls.Open = Open;
</script>
<Modal

View File

@ -23,7 +23,6 @@
export let aliases: Array<string> = [];
export let dn: string;
export let origin: Domain | DomainInList;
export let showSubdomainsList = false;
export let services: Array<ServiceCombined>;
export let zoneId: string;
@ -179,18 +178,6 @@
<Icon name="link" />
{$t('domains.add-an-alias')}
</Button>
{#if !showSubdomainsList && !dn}
<Button
type="button"
color="secondary"
outline
size="sm"
on:click={() => dispatch("new-subdomain")}
>
<Icon name="server" />
{$t('domains.add-a-subdomain')}
</Button>
{/if}
</div>
{#if showResources}
<div

View File

@ -1,10 +1,9 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import AliasModal from '$lib/components/domains/AliasModal.svelte';
import NewSubdomainModal from '$lib/components/domains/NewSubdomainModal.svelte';
import ServiceModal from '$lib/components/domains/ServiceModal.svelte';
import ServiceSelectorModal from '$lib/components/domains/ServiceSelectorModal.svelte';
import AliasModal, { controls as ctrlAlias } from '$lib/components/domains/AliasModal.svelte';
import { controls as ctrlNewService } from '$lib/components/NewServicePath.svelte';
import { controls as ctrlService } from '$lib/components/domains/ServiceModal.svelte';
import SubdomainItem from '$lib/components/domains/SubdomainItem.svelte';
import type { Domain, DomainInList } from '$lib/model/domain';
import type { ServiceCombined } from '$lib/model/service';
@ -13,7 +12,6 @@
const dispatch = createEventDispatcher();
export let origin: DomainInList | Domain;
export let showSubdomainsList: boolean;
export let sortedDomains: Array<string>;
export let zone: Zone;
@ -33,26 +31,18 @@
}
})
}
if (tmp['@']) tmp[""] = tmp["@"];
aliases = tmp;
}
export let newSubdomainModalOpened = false;
let subdomainModal = "";
let newAliasModalOpened = false;
let serviceSelectorModalOpened = false;
let serviceSelectedModal: string | null = null;
function showServiceSelectorModal(subdomain: string) {
subdomainModal = subdomain;
serviceSelectorModalOpened = true;
$: if (newSubdomainModalOpened) {
ctrlNewSubdomain.Open();
}
let serviceModalOpened = false;
let serviceModalService: ServiceCombined | null = null;
function showServiceModal(event: CustomEvent<ServiceCombined>) {
serviceModalService = event.detail;
serviceModalOpened = true;
ctrlService.Open(event.detail);
}
</script>
@ -61,43 +51,16 @@
aliases={aliases[dn]?aliases[dn]:[]}
{dn}
{origin}
{showSubdomainsList}
zoneId={zone.id}
services={zone.services[dn]?zone.services[dn]:[]}
on:new-alias={() => {subdomainModal = dn; newAliasModalOpened = true;}}
on:new-service={() => showServiceSelectorModal(dn)}
on:new-subdomain={() => newSubdomainModalOpened = true}
on:new-alias={() => ctrlAlias.Open(dn)}
on:new-service={() => ctrlNewService.Open(dn)}
on:show-service={showServiceModal}
on:update-zone-services={(event) => dispatch("update-zone-services", event.detail)}
/>
{/each}
<NewSubdomainModal
bind:isOpen={newSubdomainModalOpened}
{origin}
bind:value={subdomainModal}
on:show-next-modal={(event) => showServiceSelectorModal(event.detail)}
/>
<ServiceSelectorModal
bind:isOpen={serviceSelectorModalOpened}
dn={subdomainModal}
{origin}
bind:value={serviceSelectedModal}
zservices={zone.services}
on:show-next-modal={showServiceModal}
/>
{#if serviceModalService}
<ServiceModal
bind:isOpen={serviceModalOpened}
{origin}
service={serviceModalService}
{zone}
on:update-zone-services={(event) => dispatch("update-zone-services", event.detail)}
/>
{/if}
<AliasModal
bind:isOpen={newAliasModalOpened}
dn={subdomainModal}
{origin}
{zone}
on:update-zone-services={(event) => dispatch("update-zone-services", event.detail)}

View File

@ -41,3 +41,20 @@ export const domains_idx = derived(
return idx;
},
);
export const domains_by_groups = derived(
domains,
($domains: null|Array<DomainInList>) => {
const groups: Record<string, Array<DomainInList>> = { };
for (const domain of $domains) {
if (groups[domain.group] === undefined) {
groups[domain.group] = [];
}
groups[domain.group].push(domain);
}
return groups;
},
);

View File

@ -0,0 +1,41 @@
import { derived, writable, type Writable } from 'svelte/store';
import { domainCompare } from '$lib/dns';
import {
retrieveZone as APIRetrieveZone,
getZone as APIGetZone,
} from '$lib/api/zone';
import type { Zone } from '$lib/model/zone';
import { refreshDomains } from '$lib/stores/domains';
export const thisZone: Writable<null | Zone> = writable(null);
export const sortedDomains = derived(
thisZone,
($thisZone: null|Zone) => {
if (!$thisZone) {
return null;
}
if (!$thisZone.services) {
return [];
}
const domains = Object.keys($thisZone.services);
domains.sort(domainCompare);
return domains;
},
);
export async function getZone(domain: string, zoneId: string) {
thisZone.set(null);
const zone = await APIGetZone(domain, zoneId);
thisZone.set(zone);
return zone;
}
export async function retrieveZone(domain: string) {
const meta = await APIRetrieveZone(domain);
await refreshDomains();
return meta;
}

View File

@ -0,0 +1,12 @@
import { get_store_value } from 'svelte/internal';
import type { Load } from '@sveltejs/kit';
import { providers, refreshProviders } from '$lib/stores/providers';
export const load: Load = async({ parent }) => {
const data = await parent();
if (!get_store_value(providers)) await refreshProviders();
return data;
}

View File

@ -1,15 +1,24 @@
import { error } from '@sveltejs/kit';
import { get_store_value } from 'svelte/internal';
import type { Load } from '@sveltejs/kit';
import { domains, refreshDomains } from '$lib/stores/domains';
import { domains, domains_idx, refreshDomains } from '$lib/stores/domains';
export const load: Load = async({ parent, params }) => {
const data = await parent();
if (!get_store_value(domains)) await refreshDomains();
const domain: DomainInList | null = get_store_value(domains_idx)[params.dn];
if (!domain) {
throw error(404, {
message: 'Domain not found'
});
}
return {
domain: params.dn,
domain,
...data,
}
}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { tick } from 'svelte';
import { goto } from '$app/navigation';
import { goto, invalidateAll } from '$app/navigation';
// @ts-ignore
import { escape } from 'html-escaper';
@ -24,124 +24,70 @@
getDomain as APIGetDomain,
deleteDomain as APIDeleteDomain,
} from '$lib/api/domains';
import {
retrieveZone as APIRetrieveZone,
} from '$lib/api/zone';
import ImgProvider from '$lib/components/providers/ImgProvider.svelte';
import ModalDiffZone, { controls as ctrlDiffZone } from '$lib/components/ModalDiffZone.svelte';
import ModalDomainDelete, { controls as ctrlDomainDelete } from '$lib/components/ModalDomainDelete.svelte';
import ModalUploadZone, { controls as ctrlUploadZone } from '$lib/components/ModalUploadZone.svelte';
import ModalViewZone, { controls as ctrlViewZone } from '$lib/components/ModalViewZone.svelte';
import NewSubdomainPath, { controls as ctrlNewSubdomain } from '$lib/components/NewSubdomainPath.svelte';
import NewServicePath from '$lib/components/NewServicePath.svelte';
import NewSubdomainModal from '$lib/components/domains/NewSubdomainModal.svelte';
import ServiceModal from '$lib/components/domains/ServiceModal.svelte';
import { fqdn } from '$lib/dns';
import type { Domain, DomainInList } from '$lib/model/domain';
import type { ZoneMeta } from '$lib/model/zone';
import { domains, domains_idx, refreshDomains } from '$lib/stores/domains';
import { providers, providers_idx, refreshProviders } from '$lib/stores/providers';
import { domains, domains_by_groups, domains_idx, refreshDomains } from '$lib/stores/domains';
import { retrieveZone as StoreRetrieveZone, sortedDomains, thisZone } from '$lib/stores/thiszone';
import { t } from '$lib/translations';
export let data: {domain: string; history: string;};
export let data: {domain: DomainInList; history: string;};
let selectedDomain = data.domain;
$: if (selectedDomain != data.domain) {
main_error = null;
let selectedDomain = data.domain.domain;
$: if (selectedDomain != data.domain.domain) {
goto('/domains/' + encodeURIComponent(selectedDomain));
}
if (!$domains) refreshDomains();
if (!$providers) refreshProviders();
let domainsByGroup: Record<string, Array<DomainInList>> = {};
$: {
if ($domains) {
const tmp: Record<string, Array<DomainInList>> = { };
for (const domain of $domains) {
if (tmp[domain.group] === undefined) {
tmp[domain.group] = [];
}
tmp[domain.group].push(domain);
}
domainsByGroup = tmp;
}
}
let main_error: string | null = null;
let selectedHistory: string | undefined;
$: selectedHistory = data.history;
$: if (!data.history && $domains_idx[selectedDomain] && $domains_idx[selectedDomain].zone_history && $domains_idx[selectedDomain].zone_history.length > 0) {
selectedHistory = $domains_idx[selectedDomain].zone_history[0] as string;
}
$: if (selectedHistory && data.history != selectedHistory) {
main_error = null;
//goto('/domains/' + encodeURIComponent(selectedDomain) + '/' + encodeURIComponent(selectedHistory));
goto('/domains/' + encodeURIComponent(selectedDomain) + '/' + encodeURIComponent(selectedHistory));
}
let retrievalInProgress = false;
function retrieveZone(): void {
if (domain) {
retrievalInProgress = true;
APIRetrieveZone(domain).then(
retrieveZoneDone,
(err: any) => {
retrievalInProgress = false;
throw err;
}
);
}
async function retrieveZone(): void {
retrievalInProgress = true;
retrieveZoneDone(await StoreRetrieveZone(data.domain));
}
function retrieveZoneDone(zm: ZoneMeta): void {
retrievalInProgress = false;
refreshDomains();
selectedHistory = zm.id;
main_error = null;
if (data.history) {
selectedHistory = zm.id;
} else {
invalidateAll();
}
}
async function getDomain(id: string): Promise<Domain> {
return await APIGetDomain(id);
}
let domain: null | Domain = null;
$: if ($domains_idx[selectedDomain]) {
if (!$domains_idx[selectedDomain].zone_history || $domains_idx[selectedDomain].zone_history.length == 0) {
retrievalInProgress = true;
APIRetrieveZone($domains_idx[selectedDomain]).then(
retrieveZoneDone,
(err: any) => {
retrievalInProgress = false;
tick().then(() => {
main_error = err.toString();
});
}
)
} else {
domain = null;
getDomain($domains_idx[selectedDomain].id).then(
(dn) => {
domain = dn;
}
);
}
}
function viewZone(): void {
if (!domain || !selectedHistory) {
if (!selectedHistory) {
return;
}
ctrlViewZone.Open(domain, selectedHistory);
ctrlViewZone.Open(data.domain, selectedHistory);
}
function showDiff(): void {
if (!domain || !selectedHistory) {
if (!selectedHistory) {
return;
}
ctrlDiffZone.Open(domain, selectedHistory);
ctrlDiffZone.Open(data.domain, selectedHistory);
}
let deleteInProgress = false;
@ -187,8 +133,8 @@
type="select"
bind:value={selectedDomain}
>
{#each Object.keys(domainsByGroup) as gname}
{@const group = domainsByGroup[gname]}
{#each Object.keys($domains_by_groups) as gname}
{@const group = $domains_by_groups[gname]}
<optgroup label={gname=="undefined"?$t("domaingroups.no-group"):gname}>
{#each group as domain}
<option value={domain.domain}>{domain.domain}</option>
@ -198,7 +144,7 @@
</Input>
</div>
{#if data && data.streamed && data.streamed.sortedDomains}
{#if data && data.streamed && $sortedDomains}
<div class="d-flex gap-2 pb-2 sticky-top bg-light" style="padding-top: 10px">
<Button
type="button"
@ -206,6 +152,7 @@
outline
size="sm"
class="flex-fill"
on:click={() => ctrlNewSubdomain.Open()}
>
<Icon name="server" />
{$t('domains.add-a-subdomain')}
@ -216,19 +163,23 @@
outline
size="sm"
>
<Icon name="wrench-adjustable-circle" aria-hidden="true" />
{#if retrievalInProgress}
<Spinner size="sm" />
{:else}
<Icon name="wrench-adjustable-circle" aria-hidden="true" />
{/if}
</DropdownToggle>
<DropdownMenu>
<DropdownItem header class="font-monospace">
{data.selectedDomain.domain}
{data.domain.domain}
</DropdownItem>
<DropdownItem
href={`/domains/${data.selectedDomain.domain}/history`}
href={`/domains/${data.domain.domain}/history`}
>
{$t('domains.actions.history')}
</DropdownItem>
<DropdownItem
href={`/domains/${data.selectedDomain.domain}/logs`}
href={`/domains/${data.domain.domain}/logs`}
>
{$t('domains.actions.audit')}
</DropdownItem>
@ -266,16 +217,16 @@
</DropdownMenu>
</ButtonDropdown>
</div>
{#await data.streamed.sortedDomains then sortedDomains}
{#await data.streamed.zone then z}
<div style="min-height:0; overflow-y: auto;">
{#each sortedDomains as dn}
{#each $sortedDomains as dn}
<a
href={'#' + (dn?dn:'@')}
title={fqdn(dn, data.selectedDomain.domain)}
title={fqdn(dn, data.domain.domain)}
class="d-block text-truncate font-monospace text-muted text-decoration-none"
style={'max-width: none; padding-left: ' + (dn === '' ? 0 : (dn.split('.').length * 10)) + 'px'}
>
{fqdn(dn, data.selectedDomain.domain)}
{fqdn(dn, data.domain.domain)}
</a>
{/each}
</div>
@ -284,7 +235,7 @@
<div class="flex-fill" />
{#if domain && domain.zone_history && $domains_idx[selectedDomain] && domain.id === $domains_idx[selectedDomain].id}
{#if data.domain.zone_history && $domains_idx[selectedDomain] && data.domain.id === $domains_idx[selectedDomain].id}
<ButtonGroup class="mt-2 w-100">
{#if $domains_idx[selectedDomain].zone_history && selectedHistory === $domains_idx[selectedDomain].zone_history[0]}
<Button
@ -323,17 +274,7 @@
md={9}
class="d-flex"
>
{#if main_error}
<div class="d-flex flex-column mt-4">
<Alert
color="danger"
fade={false}
>
<strong>{$t('errors.domain-import')}</strong>
{main_error}
</Alert>
</div>
{:else if data.history == selectedHistory}
{#if data.history == selectedHistory}
<slot />
{:else}
<div class="mt-5 text-center flex-fill">
@ -345,8 +286,23 @@
</Row>
</Container>
<NewSubdomainPath
origin={data.domain}
/>
{#await data.streamed.zone then zone}
<NewServicePath
origin={data.domain}
{zone}
/>
<ServiceModal
origin={data.domain}
{zone}
on:update-zone-services={(event) => thisZone.set(event.detail)}
/>
{/await}
<ModalUploadZone
{domain}
domain={data.domain}
{selectedHistory}
on:retrieveZoneDone={retrieveZoneDone}
/>
@ -358,7 +314,7 @@
<ModalViewZone />
<ModalDiffZone
{domain}
domain={data.domain}
{selectedHistory}
on:retrieveZoneDone={retrieveZoneDone}
/>

View File

@ -2,29 +2,21 @@ import { get_store_value } from 'svelte/internal';
import { error, redirect } from '@sveltejs/kit';
import type { Load } from '@sveltejs/kit';
import { getZone } from '$lib/api/zone';
import { domainCompare } from '$lib/dns';
import { domains_idx } from '$lib/stores/domains';
import { getZone } from '$lib/stores/thiszone';
export const load: Load = async({ parent, params }) => {
const data = await parent();
const domain: DomainInList | null = get_store_value(domains_idx)[data.domain];
const domain = data.domain;
if (domain === null) {
throw error(404, {
message: 'Domain not found'
});
}
if (!domain.zone_history || domain.zone_history.length === 0) {
throw error(500, {
message: 'Domain not initialized'
});
throw redirect(307, `/domains/${data.domain.domain}/import_zone`);
}
if (!params.historyid) {
params.historyid = domain.zone_history[0];
//throw redirect(307, `/domains/${data.domain}/${domain.zone_history[0]}`);
//throw redirect(307, `/domains/${data.domain.domain}/${domain.zone_history[0]}`);
}
const zhidx = domain.zone_history.indexOf(params.historyid);
@ -38,22 +30,11 @@ export const load: Load = async({ parent, params }) => {
const zone = getZone(domain, zoneId);
const sortedDomains = zone.then((z) => {
if (!z.services) {
return [];
}
const domains = Object.keys(z.services);
domains.sort(domainCompare);
return domains;
})
return {
history: params.historyid,
selectedDomain: domain,
zoneId,
streamed: {
zone,
sortedDomains,
},
...data,
}

View File

@ -13,21 +13,20 @@
import type { Zone } from '$lib/model/zone';
import { domains_idx } from '$lib/stores/domains';
import { servicesSpecs, refreshServicesSpecs } from '$lib/stores/services';
import { retrieveZone, sortedDomains, thisZone } from '$lib/stores/thiszone';
import { t } from '$lib/translations';
if (!$servicesSpecs) refreshServicesSpecs();
export let data: {domain: string; selectedDomain: DomainInList; history: string; zoneId: string; streamed: Object};
export let newSubdomainModalOpened = false;
export let data: {domain: DomainInList; history: string; zoneId: string; streamed: Object};
</script>
{#if !data.selectedDomain}
{#if !data.domain}
<div class="mt-5 text-center flex-fill">
<Spinner label="Spinning" />
<p>{$t('wait.loading')}</p>
</div>
{:else if !data.selectedDomain.zone_history || data.selectedDomain.zone_history.length == 0}
{:else if !data.domain.zone_history || data.domain.zone_history.length == 0}
<div class="mt-4 text-center flex-fill">
<Spinner label={$t('common.spinning')} />
<p>{$t('wait.importing')}</p>
@ -39,23 +38,15 @@
<p>{$t('wait.loading')}</p>
</div>
{:then zone}
{#if zone}
{#await data.streamed.sortedDomains}
<div class="mt-4 text-center flex-fill">
<Spinner label={$t('common.spinning')} />
<p>{$t('wait.loading')}</p>
</div>
{:then sortedDomains}
<div style="max-width: 100%;" class="pt-1">
<SubdomainList
origin={data.selectedDomain}
{sortedDomains}
{zone}
bind:newSubdomainModalOpened={newSubdomainModalOpened}
on:update-zone-services={(event) => zone = event.detail}
/>
</div>
{/await}
{#if zone && $sortedDomains}
<div style="max-width: 100%;" class="pt-1">
<SubdomainList
origin={data.domain}
sortedDomains={$sortedDomains}
zone={$thisZone}
on:update-zone-services={(event) => thisZone.set(event.detail)}
/>
</div>
{/if}
{/await}
{/if}

View File

@ -0,0 +1,37 @@
<script lang="ts">
import { goto } from '$app/navigation';
import {
Alert,
Icon,
Spinner,
} from 'sveltestrap';
import type { DomainInList } from '$lib/model/domain';
import { retrieveZone } from '$lib/stores/thiszone';
import { t } from '$lib/translations';
export let data: {domain: DomainInList;};
let rz = retrieveZone(data.domain);
rz.then(() => {
goto(`/domains/${encodeURIComponent(data.domain.domain)}`);
}, (e) => { })
</script>
<div class="mt-4 text-center flex-fill">
{#await rz}
<Spinner label={$t('common.spinning')} />
<p>{$t('wait.importing')}</p>
{:then}
<p>{$t('wait.wait')}</p>
{:catch main_error}
<Alert
color="danger"
fade={false}
>
<strong>{$t('errors.domain-import')}</strong>
{main_error}
</Alert>
{/await}
</div>