happyDomain/ui/src/lib/components/domains/SubdomainItem.svelte

321 lines
11 KiB
Svelte

<!--
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 { createEventDispatcher } from 'svelte';
import {
Badge,
Button,
Icon,
Popover,
Spinner,
} from '@sveltestrap/sveltestrap';
import { deleteZoneService } from '$lib/api/zone';
import Service from '$lib/components/domains/Service.svelte';
import { fqdn, isReverseZone, unreverseDomain } from '$lib/dns';
import type { Domain, DomainInList } from '$lib/model/domain';
import type { ServiceCombined } from '$lib/model/service';
import { ZoneViewGrid } from '$lib/model/usersettings';
import { servicesSpecs } from '$lib/stores/services';
import { userSession } from '$lib/stores/usersession';
import { t } from '$lib/translations';
const dispatch = createEventDispatcher();
export let aliases: Array<string> = [];
export let dn: string;
export let origin: Domain | DomainInList;
export let services: Array<ServiceCombined>;
export let zoneId: string;
let reverseZone = false;
$: reverseZone = isReverseZone(origin.domain);
let showResources = true;
function isCNAME(services: Array<ServiceCombined>) {
return services.length === 1 && services[0]._svctype === 'svcs.CNAME';
}
function isPTR(services: Array<ServiceCombined>) {
return services.length === 1 && services[0]._svctype === 'svcs.PTR';
}
let deleteServiceInProgress = false;
function deleteCNAME() {
deleteServiceInProgress = true;
deleteZoneService(origin, zoneId, services[0]).then(
(z) => {
dispatch("update-zone-services", z);
deleteServiceInProgress = false;
},
(err) => {
deleteServiceInProgress = false;
throw err;
}
);
}
function showServiceModal(service: ServiceCombined) {
dispatch("show-service", service);
}
</script>
{#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}>
<h2
class="sticky-top"
style="background: white; z-index: 1"
>
<span style="white-space: nowrap">
{#if isPTR(services)}
<Icon
name="signpost"
title="PTR"
/>
{:else}
<Icon
name="sign-turn-right"
title="CNAME"
/>
{/if}
<span
class="font-monospace"
title={fqdn(dn, origin.domain)}
>
{#if reverseZone}
{unreverseDomain(fqdn(dn, origin.domain))}
{:else}
{fqdn(dn, origin.domain)}
{/if}
</span>
</span>
<span style="white-space: nowrap">
<Icon name="arrow-right" />
<span class="font-monospace">
{services[0].Service.Target}
</span>
</span>
<Button
type="button"
color="primary"
size="sm"
class="ml-2"
on:click={() => dispatch("new-service")}
>
<Icon name="plus" />
{$t('service.add')}
</Button>
<Button
type="button"
color="info"
outline
size="sm"
class="ml-2"
on:click={() => showServiceModal(services[0])}
>
<Icon name="pencil" />
{$t('domains.edit-target')}
</Button>
<Button
type="button"
color="danger"
disabled={deleteServiceInProgress}
outline
size="sm"
class="ml-2"
on:click={deleteCNAME}
>
{#if deleteServiceInProgress}
<Spinner size="sm" />
{:else}
<Icon name="x-circle" />
{/if}
{#if isPTR(services)}
{$t('domains.drop-pointer')}
{:else}
{$t('domains.drop-alias')}
{/if}
</Button>
</h2>
</div>
{:else}
<div id={dn?dn:'@'}>
<div
class="d-flex align-items-center sticky-top mb-2 gap-2"
style="background: white; z-index: 1"
>
<h2
style="white-space: nowrap; cursor: pointer;"
class="mb-0 text-truncate"
on:click={() => showResources = !showResources}
on:keypress={() => showResources = !showResources}
>
{#if showResources}
<Icon name="chevron-down" />
{:else}
<Icon name="chevron-right" />
{/if}
<span
class="font-monospace"
title={fqdn(dn, origin.domain)}
>
{#if reverseZone}
{unreverseDomain(fqdn(dn, origin.domain))}
{:else}
{fqdn(dn, origin.domain)}
{/if}
</span>
</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}
<Badge
id={"popoverbadge-" + dn.replace('.', '__')}
style="cursor: pointer;"
>
+ {$t('domains.n-aliases', {count: aliases.length})}
</Badge>
<Popover
dismissible
placement="bottom"
target={"popoverbadge-" + dn.replace('.', '__')}
class="font-monospace"
>
{#each aliases as alias}
<a href={"#" + alias}>
{alias}
</a>
<br>
{/each}
</Popover>
{/if}
{#if !showResources || ($userSession && $userSession.settings.zoneview !== ZoneViewGrid)}
<Button
type="button"
color="primary"
size="sm"
on:click={() => dispatch("new-service")}
>
<Icon name="plus" />
{$t('domains.add-a-service')}
</Button>
{/if}
{#if showResources}
<Button
type="button"
color="primary"
outline
size="sm"
on:click={() => dispatch("new-alias")}
>
<Icon name="link" />
{$t('domains.add-an-alias')}
</Button>
{/if}
</div>
{#if showResources}
<div
class:d-flex={showResources && $userSession && $userSession.settings.zoneview === ZoneViewGrid}
class:justify-content-around={showResources && $userSession && $userSession.settings.zoneview === ZoneViewGrid}
class:flex-wrap={showResources && $userSession && $userSession.settings.zoneview === ZoneViewGrid}
>
{#each services as service}
{#key service}
<Service
{origin}
{service}
{zoneId}
on:show-service={(event) => showServiceModal(event.detail)}
on:update-zone-services={(event) => dispatch("update-zone-services", event.detail)}
/>
{/key}
{/each}
{#if $userSession && $userSession.settings.zoneview === ZoneViewGrid}
<Service
{origin}
{zoneId}
on:show-service={() => dispatch("new-service")}
on:update-zone-services={(event) => dispatch("update-zone-services", event.detail)}
/>
{/if}
</div>
{/if}
</div>
{/if}