Compare commits

...

4 Commits

10 changed files with 160 additions and 24 deletions

View File

@ -98,7 +98,7 @@
</ModalFooter> </ModalFooter>
{/if} {/if}
<ModalFooter> <ModalFooter>
{#if origin && zoneId} {#if update && origin && zoneId}
<Button <Button
color="dark" color="dark"
outline={!showRecords} outline={!showRecords}

View File

@ -38,6 +38,7 @@
import type { Domain, DomainInList } from '$lib/model/domain'; import type { Domain, DomainInList } from '$lib/model/domain';
import type { ServiceCombined } from '$lib/model/service'; import type { ServiceCombined } from '$lib/model/service';
import { ZoneViewGrid } from '$lib/model/usersettings'; import { ZoneViewGrid } from '$lib/model/usersettings';
import { servicesSpecs } from '$lib/stores/services';
import { userSession } from '$lib/stores/usersession'; import { userSession } from '$lib/stores/usersession';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
@ -82,7 +83,43 @@
} }
</script> </script>
{#if isCNAME(services) || isPTR(services)} {#if services.length === 0}
<div id={dn}>
{#if !reverseZone}
<h2
class="sticky-top"
style="background: white; z-index: 1"
>
<span style="white-space: nowrap">
<Icon
name="plus-square-dotted"
title="Intermediate domain with no service"
/>
<span
class="font-monospace"
title={fqdn(dn, origin.domain)}
>
{#if reverseZone}
{unreverseDomain(fqdn(dn, origin.domain))}
{:else}
{fqdn(dn, origin.domain)}
{/if}
</span>
</span>
<Button
type="button"
color="primary"
size="sm"
class="ml-2"
on:click={() => dispatch("new-service")}
>
<Icon name="plus" />
{$t('service.add')}
</Button>
</h2>
{/if}
</div>
{:else if isCNAME(services) || isPTR(services)}
<div id={dn}> <div id={dn}>
<h2 <h2
class="sticky-top" class="sticky-top"
@ -188,12 +225,31 @@
{/if} {/if}
</span> </span>
</h2> </h2>
{#if !showResources}
<Badge
id={"popoversvc-" + dn.replace('.', '__')}
style="cursor: pointer;"
>
{$t('domains.n-services', {count: services.length})}
</Badge>
<Popover
dismissible
placement="bottom"
target={"popoversvc-" + dn.replace('.', '__')}
>
{#each services as service}
<strong>{$servicesSpecs[service._svctype].name}:</strong>
<span class="text-muted">{service._comment}</span>
<br>
{/each}
</Popover>
{/if}
{#if aliases.length != 0} {#if aliases.length != 0}
<Badge <Badge
id={"popoverbadge-" + dn.replace('.', '__')} id={"popoverbadge-" + dn.replace('.', '__')}
style="cursor: pointer;" style="cursor: pointer;"
> >
+ {$t('domains.n-aliases', {n: aliases.length})} + {$t('domains.n-aliases', {count: aliases.length})}
</Badge> </Badge>
<Popover <Popover
dismissible dismissible
@ -209,7 +265,7 @@
{/each} {/each}
</Popover> </Popover>
{/if} {/if}
{#if $userSession && $userSession.settings.zoneview !== ZoneViewGrid} {#if !showResources || ($userSession && $userSession.settings.zoneview !== ZoneViewGrid)}
<Button <Button
type="button" type="button"
color="primary" color="primary"
@ -220,6 +276,7 @@
{$t('domains.add-a-service')} {$t('domains.add-a-service')}
</Button> </Button>
{/if} {/if}
{#if showResources}
<Button <Button
type="button" type="button"
color="primary" color="primary"
@ -230,6 +287,7 @@
<Icon name="link" /> <Icon name="link" />
{$t('domains.add-an-alias')} {$t('domains.add-an-alias')}
</Button> </Button>
{/if}
</div> </div>
{#if showResources} {#if showResources}
<div <div

View File

@ -36,6 +36,7 @@
export let origin: DomainInList | Domain; export let origin: DomainInList | Domain;
export let sortedDomains: Array<string>; export let sortedDomains: Array<string>;
export let sortedDomainsWithIntermediate: Array<string>;
export let zone: Zone; export let zone: Zone;
let aliases: Record<string, Array<string>>; let aliases: Record<string, Array<string>>;
@ -69,7 +70,7 @@
} }
</script> </script>
{#each sortedDomains as dn} {#each sortedDomainsWithIntermediate as dn}
<SubdomainItem <SubdomainItem
aliases={aliases[dn]?aliases[dn]:[]} aliases={aliases[dn]?aliases[dn]:[]}
{dn} {dn}

View File

@ -0,0 +1,41 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2024 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type { DomainInList } from '$lib/model/domain';
import { fqdn } from '$lib/dns';
export let domains: Array<string>;
export let origin: DomainInList;
</script>
{#each domains as dn}
<a
href={'#' + (dn?dn:'@')}
title={fqdn(dn, origin.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, origin.domain)}
</a>
{/each}

View File

@ -108,7 +108,8 @@
"give-explicit-name": "Give an explicit name in order to easily find this service.", "give-explicit-name": "Give an explicit name in order to easily find this service.",
"history": "History", "history": "History",
"list": "List importable domains", "list": "List importable domains",
"n-aliases": "{{n:lt; 2:{{n}} alias; default:{{n}} aliases}}", "n-aliases": "{{count:lt; 2:{{count}} alias; default:{{count}} aliases}}",
"n-services": "{{count:lt; 2:{{count}} service; default:{{count}} services}}",
"please-fill-fields": "Please fill the following fields:", "please-fill-fields": "Please fill the following fields:",
"removal": "Confirm Domain Removal", "removal": "Confirm Domain Removal",
"save-modifications": "Save those modifications", "save-modifications": "Save those modifications",

View File

@ -108,7 +108,8 @@
"give-explicit-name": "Donnez un nom explicite afin de trouver facilement ce service.", "give-explicit-name": "Donnez un nom explicite afin de trouver facilement ce service.",
"history": "Historique", "history": "Historique",
"list": "Liste des domaines importables", "list": "Liste des domaines importables",
"n-aliases": "{{n:lt; 2:{{n}} alias; default:{{n}} alias}}", "n-aliases": "{{count:lt; 2:{{count}} alias; default:{{count}} alias}}",
"n-services": "{{count:lt; 2:{{count}} service; default:{{count}} services}}",
"please-fill-fields": "Veuillez remplir les champs suivants:", "please-fill-fields": "Veuillez remplir les champs suivants:",
"removal": "Confirmer la suppression du domaine", "removal": "Confirmer la suppression du domaine",
"save-modifications": "Enregistrer ces modifications", "save-modifications": "Enregistrer ces modifications",

View File

@ -101,7 +101,7 @@ export class ProviderForm {
this.nextInProgress = true; this.nextInProgress = true;
this.saveState(); this.saveState();
if (this.form && this.form.nextButtonLink) { if (this.form && this.form.nextButtonLink) {
goto(this.form.nextButtonLink); window.location.href = this.form.nextButtonLink;
} else { } else {
this.form = await this.changeState(this.form && this.form.nextButtonState ? this.form.nextButtonState : 0); this.form = await this.changeState(this.form && this.form.nextButtonState ? this.form.nextButtonState : 0);
} }
@ -112,7 +112,7 @@ export class ProviderForm {
this.previousInProgress = true; this.previousInProgress = true;
this.saveState(); this.saveState();
if (this.form && this.form.previousButtonLink) { if (this.form && this.form.previousButtonLink) {
goto(this.form.previousButtonLink); window.location.href = this.form.previousButtonLink;
} else { } else {
this.form = await this.changeState(this.form && this.form.previousButtonState ? this.form.previousButtonState : 0); this.form = await this.changeState(this.form && this.form.previousButtonState ? this.form.previousButtonState : 0);
} }

View File

@ -31,6 +31,7 @@ import { refreshDomains } from '$lib/stores/domains';
export const thisZone: Writable<null | Zone> = writable(null); export const thisZone: Writable<null | Zone> = writable(null);
// sortedDomains returns all subdomains, sorted
export const sortedDomains = derived( export const sortedDomains = derived(
thisZone, thisZone,
($thisZone: null|Zone) => { ($thisZone: null|Zone) => {
@ -46,6 +47,36 @@ export const sortedDomains = derived(
}, },
); );
// sortedDomainsWithIntermediate returns all subdomains, sorted, with intermediate subdomains
export const sortedDomainsWithIntermediate = derived(
sortedDomains,
($sortedDomains: null|Array<string>) => {
if (!$sortedDomains || $sortedDomains.length <= 1) {
return $sortedDomains;
}
const domains: Array<string> = [$sortedDomains[0]];
let previous = domains[0].split('.');
for (let i = 1; i < $sortedDomains.length; i++) {
const cur = $sortedDomains[i].split('.');
if (previous.length < cur.length && previous[0] !== cur[cur.length - previous.length]) {
domains.push(cur.slice(cur.length - previous.length).join('.'));
}
while (previous.length + 1 < cur.length) {
previous = cur.slice(cur.length - previous.length - 1);
domains.push(previous.join('.'));
}
domains.push(cur.join('.'));
previous = cur;
}
return domains;
},
);
export async function getZone(domain: DomainInList | Domain, zoneId: string) { export async function getZone(domain: DomainInList | Domain, zoneId: string) {
thisZone.set(null); thisZone.set(null);

View File

@ -57,11 +57,12 @@
import ModalViewZone, { controls as ctrlViewZone } from '$lib/components/ModalViewZone.svelte'; import ModalViewZone, { controls as ctrlViewZone } from '$lib/components/ModalViewZone.svelte';
import NewSubdomainPath, { controls as ctrlNewSubdomain } from '$lib/components/NewSubdomainPath.svelte'; import NewSubdomainPath, { controls as ctrlNewSubdomain } from '$lib/components/NewSubdomainPath.svelte';
import NewSubdomainModal from '$lib/components/domains/NewSubdomainModal.svelte'; import NewSubdomainModal from '$lib/components/domains/NewSubdomainModal.svelte';
import { fqdn } from '$lib/dns'; import SubdomainListTiny from '$lib/components/domains/SubdomainListTiny.svelte';
import { fqdn, isReverseZone } from '$lib/dns';
import type { Domain, DomainInList } from '$lib/model/domain'; import type { Domain, DomainInList } from '$lib/model/domain';
import type { ZoneMeta } from '$lib/model/zone'; import type { ZoneMeta } from '$lib/model/zone';
import { domains, domains_by_groups, domains_idx, refreshDomains } from '$lib/stores/domains'; import { domains, domains_by_groups, domains_idx, refreshDomains } from '$lib/stores/domains';
import { retrieveZone as StoreRetrieveZone, sortedDomains, thisZone } from '$lib/stores/thiszone'; import { retrieveZone as StoreRetrieveZone, sortedDomains, sortedDomainsWithIntermediate, thisZone } from '$lib/stores/thiszone';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
export let data: {domain: DomainInList;}; export let data: {domain: DomainInList;};
@ -175,7 +176,7 @@
<Icon name="chevron-left" /> <Icon name="chevron-left" />
Retour à la zone Retour à la zone
</Button> </Button>
{:else if $page.data.streamed && $sortedDomains} {:else if $page.data.streamed && $sortedDomainsWithIntermediate}
<div class="d-flex gap-2 pb-2 sticky-top bg-light" style="padding-top: 10px"> <div class="d-flex gap-2 pb-2 sticky-top bg-light" style="padding-top: 10px">
<Button <Button
type="button" type="button"
@ -250,16 +251,17 @@
</div> </div>
{#await $page.data.streamed.zone then z} {#await $page.data.streamed.zone then z}
<div style="min-height:0; overflow-y: auto;"> <div style="min-height:0; overflow-y: auto;">
{#each $sortedDomains as dn} {#if isReverseZone(data.domain.domain)}
<a <SubdomainListTiny
href={'#' + (dn?dn:'@')} domains={$sortedDomains}
title={fqdn(dn, data.domain.domain)} origin={data.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'} {:else}
> <SubdomainListTiny
{fqdn(dn, data.domain.domain)} domains={$sortedDomainsWithIntermediate}
</a> origin={data.domain}
{/each} />
{/if}
</div> </div>
{/await} {/await}
{/if} {/if}
@ -267,7 +269,7 @@
<div class="flex-fill" /> <div class="flex-fill" />
{#if $page.data.isZonePage && data.domain.zone_history && $domains_idx[selectedDomain] && data.domain.id === $domains_idx[selectedDomain].id} {#if $page.data.isZonePage && data.domain.zone_history && $domains_idx[selectedDomain] && data.domain.id === $domains_idx[selectedDomain].id}
{#if !($page.data.streamed && $sortedDomains)} {#if !($page.data.streamed && $sortedDomainsWithIntermediate)}
<Button <Button
color="danger" color="danger"
class="mt-3" class="mt-3"

View File

@ -35,7 +35,7 @@
import type { Zone } from '$lib/model/zone'; import type { Zone } from '$lib/model/zone';
import { domains_idx } from '$lib/stores/domains'; import { domains_idx } from '$lib/stores/domains';
import { servicesSpecs, refreshServicesSpecs } from '$lib/stores/services'; import { servicesSpecs, refreshServicesSpecs } from '$lib/stores/services';
import { retrieveZone, sortedDomains, thisZone } from '$lib/stores/thiszone'; import { retrieveZone, sortedDomains, sortedDomainsWithIntermediate, thisZone } from '$lib/stores/thiszone';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
if (!$servicesSpecs) refreshServicesSpecs(); if (!$servicesSpecs) refreshServicesSpecs();
@ -65,6 +65,7 @@
<SubdomainList <SubdomainList
origin={data.domain} origin={data.domain}
sortedDomains={$sortedDomains} sortedDomains={$sortedDomains}
sortedDomainsWithIntermediate={$sortedDomainsWithIntermediate}
zone={$thisZone} zone={$thisZone}
on:update-zone-services={(event) => thisZone.set(event.detail)} on:update-zone-services={(event) => thisZone.set(event.detail)}
/> />