mirror of
https://github.com/unraid/api.git
synced 2026-02-15 20:48:30 -06:00
refactor: port conflict frontend stuff
This commit is contained in:
@@ -7,6 +7,7 @@ import ContainerSizesModal from '@/components/Docker/ContainerSizesModal.vue';
|
||||
import { GET_DOCKER_CONTAINERS } from '@/components/Docker/docker-containers.query';
|
||||
import DockerAutostartSettings from '@/components/Docker/DockerAutostartSettings.vue';
|
||||
import DockerContainersTable from '@/components/Docker/DockerContainersTable.vue';
|
||||
import DockerPortConflictsAlert from '@/components/Docker/DockerPortConflictsAlert.vue';
|
||||
import DockerSidebarTree from '@/components/Docker/DockerSidebarTree.vue';
|
||||
import DockerEdit from '@/components/Docker/Edit.vue';
|
||||
import DockerLogs from '@/components/Docker/Logs.vue';
|
||||
@@ -14,6 +15,12 @@ import DockerOverview from '@/components/Docker/Overview.vue';
|
||||
import DockerPreview from '@/components/Docker/Preview.vue';
|
||||
import { useDockerEditNavigation } from '@/composables/useDockerEditNavigation';
|
||||
|
||||
import type {
|
||||
ContainerPortConflict,
|
||||
DockerPortConflictsResult,
|
||||
LanPortConflict,
|
||||
PortConflictContainer,
|
||||
} from '@/components/Docker/docker-port-conflicts.types';
|
||||
import type { DockerContainer, FlatOrganizerEntry } from '@/composables/gql/graphql';
|
||||
import type { LocationQueryRaw } from 'vue-router';
|
||||
|
||||
@@ -21,29 +28,6 @@ interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type PortConflictContainer = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type LanPortConflict = {
|
||||
lanIpPort: string;
|
||||
publicPort?: number | null;
|
||||
type: string;
|
||||
containers: PortConflictContainer[];
|
||||
};
|
||||
|
||||
type ContainerPortConflict = {
|
||||
privatePort: number;
|
||||
type: string;
|
||||
containers: PortConflictContainer[];
|
||||
};
|
||||
|
||||
type DockerPortConflictsResult = {
|
||||
lanPorts: LanPortConflict[];
|
||||
containerPorts: ContainerPortConflict[];
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
});
|
||||
@@ -219,10 +203,9 @@ const containerPortConflicts = computed<ContainerPortConflict[]>(
|
||||
() => portConflicts.value?.containerPorts ?? []
|
||||
);
|
||||
|
||||
const totalPortConflictCount = computed(
|
||||
() => lanPortConflicts.value.length + containerPortConflicts.value.length
|
||||
const hasPortConflicts = computed(
|
||||
() => lanPortConflicts.value.length + containerPortConflicts.value.length > 0
|
||||
);
|
||||
const hasPortConflicts = computed(() => totalPortConflictCount.value > 0);
|
||||
|
||||
const { navigateToEditPage } = useDockerEditNavigation();
|
||||
|
||||
@@ -249,23 +232,6 @@ function handleConflictContainerAction(conflictContainer: PortConflictContainer)
|
||||
focusContainerFromConflict(conflictContainer.id);
|
||||
}
|
||||
|
||||
function formatLanConflictLabel(conflict: LanPortConflict): string {
|
||||
if (!conflict) return '';
|
||||
const lanValue = conflict.lanIpPort?.trim?.().length
|
||||
? conflict.lanIpPort
|
||||
: conflict.publicPort?.toString() || 'LAN port';
|
||||
const protocol = conflict.type || 'TCP';
|
||||
return `${lanValue} (${protocol})`;
|
||||
}
|
||||
|
||||
function formatContainerConflictLabel(conflict: ContainerPortConflict): string {
|
||||
if (!conflict) return '';
|
||||
const containerValue =
|
||||
typeof conflict.privatePort === 'number' ? conflict.privatePort : 'Container port';
|
||||
const protocol = conflict.type || 'TCP';
|
||||
return `${containerValue}/${protocol}`;
|
||||
}
|
||||
|
||||
watch(activeId, (id) => {
|
||||
if (id && viewMode.value === 'autostart') {
|
||||
viewMode.value = 'overview';
|
||||
@@ -416,85 +382,12 @@ const isDetailsDisabled = computed(() => props.disabled || isSwitching.value);
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasPortConflicts"
|
||||
class="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 dark:border-amber-400/50 dark:bg-amber-400/10 dark:text-amber-100"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<UIcon
|
||||
name="i-lucide-triangle-alert"
|
||||
class="mt-1 h-5 w-5 flex-shrink-0 text-amber-500 dark:text-amber-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="w-full space-y-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">
|
||||
Port conflicts detected ({{ totalPortConflictCount }})
|
||||
</p>
|
||||
<p class="text-xs text-amber-900/80 dark:text-amber-100/80">
|
||||
Multiple containers are sharing the same LAN or container ports. Click a container
|
||||
below to open its editor or highlight it in the table.
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div v-if="lanPortConflicts.length" class="space-y-2">
|
||||
<p
|
||||
class="text-xs font-semibold tracking-wide text-amber-900/70 uppercase dark:text-amber-100/70"
|
||||
>
|
||||
LAN ports
|
||||
</p>
|
||||
<div
|
||||
v-for="conflict in lanPortConflicts"
|
||||
:key="`lan-${conflict.lanIpPort}-${conflict.type}`"
|
||||
class="rounded-md border border-amber-200/70 bg-white/80 p-3 dark:border-amber-300/30 dark:bg-transparent"
|
||||
>
|
||||
<div class="text-sm font-medium">{{ formatLanConflictLabel(conflict) }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="container in conflict.containers"
|
||||
:key="container.id"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-1 text-xs font-medium text-amber-900 transition hover:bg-amber-200 focus-visible:ring-2 focus-visible:ring-amber-400 focus-visible:outline-none dark:border-amber-200/40 dark:bg-transparent dark:text-amber-100"
|
||||
:title="`Edit ${container.name || 'container'}`"
|
||||
@click="handleConflictContainerAction(container)"
|
||||
>
|
||||
<span>{{ container.name || 'Container' }}</span>
|
||||
<UIcon name="i-lucide-pencil" class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="containerPortConflicts.length" class="space-y-2">
|
||||
<p
|
||||
class="text-xs font-semibold tracking-wide text-amber-900/70 uppercase dark:text-amber-100/70"
|
||||
>
|
||||
Container ports
|
||||
</p>
|
||||
<div
|
||||
v-for="conflict in containerPortConflicts"
|
||||
:key="`container-${conflict.privatePort}-${conflict.type}`"
|
||||
class="rounded-md border border-amber-200/70 bg-white/80 p-3 dark:border-amber-300/30 dark:bg-transparent"
|
||||
>
|
||||
<div class="text-sm font-medium">{{ formatContainerConflictLabel(conflict) }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="container in conflict.containers"
|
||||
:key="container.id"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-1 text-xs font-medium text-amber-900 transition hover:bg-amber-200 focus-visible:ring-2 focus-visible:ring-amber-400 focus-visible:outline-none dark:border-amber-200/40 dark:bg-transparent dark:text-amber-100"
|
||||
:title="`Edit ${container.name || 'container'}`"
|
||||
@click="handleConflictContainerAction(container)"
|
||||
>
|
||||
<span>{{ container.name || 'Container' }}</span>
|
||||
<UIcon name="i-lucide-pencil" class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasPortConflicts" class="mb-4">
|
||||
<DockerPortConflictsAlert
|
||||
:lan-conflicts="lanPortConflicts"
|
||||
:container-conflicts="containerPortConflicts"
|
||||
@container:select="handleConflictContainerAction"
|
||||
/>
|
||||
</div>
|
||||
<DockerContainersTable
|
||||
:containers="containers"
|
||||
|
||||
126
web/src/components/Docker/DockerPortConflictsAlert.vue
Normal file
126
web/src/components/Docker/DockerPortConflictsAlert.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type {
|
||||
ContainerPortConflict,
|
||||
LanPortConflict,
|
||||
PortConflictContainer,
|
||||
} from '@/components/Docker/docker-port-conflicts.types';
|
||||
|
||||
interface Props {
|
||||
lanConflicts?: LanPortConflict[];
|
||||
containerConflicts?: ContainerPortConflict[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
lanConflicts: () => [],
|
||||
containerConflicts: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ (e: 'container:select', payload: PortConflictContainer): void }>();
|
||||
|
||||
const totalPortConflictCount = computed(
|
||||
() => props.lanConflicts.length + props.containerConflicts.length
|
||||
);
|
||||
|
||||
function handleConflictContainerAction(conflictContainer: PortConflictContainer) {
|
||||
emit('container:select', conflictContainer);
|
||||
}
|
||||
|
||||
function formatLanConflictLabel(conflict: LanPortConflict): string {
|
||||
if (!conflict) return '';
|
||||
const lanValue = conflict.lanIpPort?.trim?.().length
|
||||
? conflict.lanIpPort
|
||||
: conflict.publicPort?.toString() || 'LAN port';
|
||||
const protocol = conflict.type || 'TCP';
|
||||
return `${lanValue} (${protocol})`;
|
||||
}
|
||||
|
||||
function formatContainerConflictLabel(conflict: ContainerPortConflict): string {
|
||||
if (!conflict) return '';
|
||||
const containerValue =
|
||||
typeof conflict.privatePort === 'number' ? conflict.privatePort : 'Container port';
|
||||
const protocol = conflict.type || 'TCP';
|
||||
return `${containerValue}/${protocol}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 dark:border-amber-400/50 dark:bg-amber-400/10 dark:text-amber-100"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<UIcon
|
||||
name="i-lucide-triangle-alert"
|
||||
class="mt-1 h-5 w-5 flex-shrink-0 text-amber-500 dark:text-amber-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="w-full space-y-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Port conflicts detected ({{ totalPortConflictCount }})</p>
|
||||
<p class="text-xs text-amber-900/80 dark:text-amber-100/80">
|
||||
Multiple containers are sharing the same LAN or container ports. Click a container below to
|
||||
open its editor or highlight it in the table.
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div v-if="lanConflicts.length" class="space-y-2">
|
||||
<p
|
||||
class="text-xs font-semibold tracking-wide text-amber-900/70 uppercase dark:text-amber-100/70"
|
||||
>
|
||||
LAN ports
|
||||
</p>
|
||||
<div
|
||||
v-for="conflict in lanConflicts"
|
||||
:key="`lan-${conflict.lanIpPort}-${conflict.type}`"
|
||||
class="rounded-md border border-amber-200/70 bg-white/80 p-3 dark:border-amber-300/30 dark:bg-transparent"
|
||||
>
|
||||
<div class="text-sm font-medium">{{ formatLanConflictLabel(conflict) }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="container in conflict.containers"
|
||||
:key="container.id"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-1 text-xs font-medium text-amber-900 transition hover:bg-amber-200 focus-visible:ring-2 focus-visible:ring-amber-400 focus-visible:outline-none dark:border-amber-200/40 dark:bg-transparent dark:text-amber-100"
|
||||
:title="`Edit ${container.name || 'container'}`"
|
||||
@click="handleConflictContainerAction(container)"
|
||||
>
|
||||
<span>{{ container.name || 'Container' }}</span>
|
||||
<UIcon name="i-lucide-pencil" class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="containerConflicts.length" class="space-y-2">
|
||||
<p
|
||||
class="text-xs font-semibold tracking-wide text-amber-900/70 uppercase dark:text-amber-100/70"
|
||||
>
|
||||
Container ports
|
||||
</p>
|
||||
<div
|
||||
v-for="conflict in containerConflicts"
|
||||
:key="`container-${conflict.privatePort}-${conflict.type}`"
|
||||
class="rounded-md border border-amber-200/70 bg-white/80 p-3 dark:border-amber-300/30 dark:bg-transparent"
|
||||
>
|
||||
<div class="text-sm font-medium">{{ formatContainerConflictLabel(conflict) }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="container in conflict.containers"
|
||||
:key="container.id"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-1 text-xs font-medium text-amber-900 transition hover:bg-amber-200 focus-visible:ring-2 focus-visible:ring-amber-400 focus-visible:outline-none dark:border-amber-200/40 dark:bg-transparent dark:text-amber-100"
|
||||
:title="`Edit ${container.name || 'container'}`"
|
||||
@click="handleConflictContainerAction(container)"
|
||||
>
|
||||
<span>{{ container.name || 'Container' }}</span>
|
||||
<UIcon name="i-lucide-pencil" class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
22
web/src/components/Docker/docker-port-conflicts.types.ts
Normal file
22
web/src/components/Docker/docker-port-conflicts.types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface PortConflictContainer {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LanPortConflict {
|
||||
lanIpPort: string;
|
||||
publicPort?: number | null;
|
||||
type: string;
|
||||
containers: PortConflictContainer[];
|
||||
}
|
||||
|
||||
export interface ContainerPortConflict {
|
||||
privatePort: number;
|
||||
type: string;
|
||||
containers: PortConflictContainer[];
|
||||
}
|
||||
|
||||
export interface DockerPortConflictsResult {
|
||||
lanPorts: LanPortConflict[];
|
||||
containerPorts: ContainerPortConflict[];
|
||||
}
|
||||
Reference in New Issue
Block a user