mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
feat: improve Dozzle Cloud discoverability (#4609)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vendored
+2
@@ -205,6 +205,7 @@ declare global {
|
||||
const useClipboard: typeof import('@vueuse/core').useClipboard
|
||||
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
|
||||
const useCloned: typeof import('@vueuse/core').useCloned
|
||||
const useCloudConfig: typeof import('./composable/cloudConfig').useCloudConfig
|
||||
const useColorMode: typeof import('@vueuse/core').useColorMode
|
||||
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
|
||||
const useContainerActions: typeof import('./composable/containerActions').useContainerActions
|
||||
@@ -634,6 +635,7 @@ declare module 'vue' {
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useCloudConfig: UnwrapRef<typeof import('./composable/cloudConfig')['useCloudConfig']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
|
||||
|
||||
Vendored
+3
-1
@@ -36,6 +36,8 @@ declare module 'vue' {
|
||||
'Cil:columns': typeof import('~icons/cil/columns')['default']
|
||||
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
|
||||
CloudDestinationForm: typeof import('./components/Notification/CloudDestinationForm.vue')['default']
|
||||
CloudPopover: typeof import('./components/CloudPopover.vue')['default']
|
||||
CloudSettingsCard: typeof import('./components/CloudSettingsCard.vue')['default']
|
||||
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
|
||||
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
|
||||
ContainerDropdown: typeof import('./components/ContainerDropdown.vue')['default']
|
||||
@@ -102,7 +104,6 @@ declare module 'vue' {
|
||||
'Mdi:chartBar': typeof import('~icons/mdi/chart-bar')['default']
|
||||
'Mdi:chartLine': typeof import('~icons/mdi/chart-line')['default']
|
||||
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
||||
'Mdi:checkCircle': typeof import('~icons/mdi/check-circle')['default']
|
||||
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
|
||||
'Mdi:chevronDown': typeof import('~icons/mdi/chevron-down')['default']
|
||||
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
|
||||
@@ -121,6 +122,7 @@ declare module 'vue' {
|
||||
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
|
||||
'Mdi:linkVariant': typeof import('~icons/mdi/link-variant')['default']
|
||||
'Mdi:linkVariantOff': typeof import('~icons/mdi/link-variant-off')['default']
|
||||
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
||||
'Mdi:pencilOutline': typeof import('~icons/mdi/pencil-outline')['default']
|
||||
'Mdi:plus': typeof import('~icons/mdi/plus')['default']
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<Dropdown class="dropdown-end" @click="onOpen">
|
||||
<template #trigger>
|
||||
<div class="relative">
|
||||
<mdi:cloud
|
||||
class="size-6"
|
||||
:class="
|
||||
!cloudConfig ? 'text-base-content/40' : cloudConfig.linked && !cloudStatusError ? 'text-info' : 'text-error'
|
||||
"
|
||||
/>
|
||||
<span
|
||||
v-if="cloudConfig?.linked"
|
||||
class="absolute -top-0.5 -right-0.5 size-2 rounded-full"
|
||||
:class="cloudStatusError ? 'bg-error' : 'bg-success'"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="w-80 space-y-3 p-1">
|
||||
<!-- Not linked -->
|
||||
<template v-if="!cloudConfig">
|
||||
<div class="flex flex-col items-center gap-2 p-2 text-center">
|
||||
<mdi:cloud class="text-base-content/40 text-4xl" />
|
||||
<h3 class="text-base font-bold">{{ $t("cloud.title") }}</h3>
|
||||
<p class="text-base-content/60 text-sm">{{ $t("cloud.description") }}</p>
|
||||
<div class="mt-2 flex w-full gap-2">
|
||||
<a :href="`${cloudUrl}`" target="_blank" rel="noreferrer noopener" class="btn btn-sm flex-1">
|
||||
{{ $t("cloud.learn-more") }}
|
||||
</a>
|
||||
<a :href="cloudLinkUrl" class="btn btn-primary btn-sm flex-1">
|
||||
<mdi:link-variant class="text-base" />
|
||||
{{ $t("cloud.link-instance") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Linked -->
|
||||
<template v-else-if="cloudConfig.linked">
|
||||
<!-- Error state -->
|
||||
<div v-if="cloudStatusError" class="space-y-3">
|
||||
<div class="alert alert-error">
|
||||
<mdi:alert-circle class="text-lg" />
|
||||
<span class="text-sm">{{ $t("cloud.error") }}</span>
|
||||
</div>
|
||||
<a :href="cloudLinkUrl" class="btn btn-primary btn-sm w-full">
|
||||
<mdi:link-variant class="text-base" />
|
||||
{{ $t("cloud.relink-instance") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-else-if="isLoadingCloudStatus" class="flex items-center justify-center gap-2 py-4">
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
</div>
|
||||
|
||||
<!-- Healthy -->
|
||||
<div v-else-if="cloudStatus" class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-bold">{{ $t("cloud.title") }}</h3>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="badge badge-success badge-sm">{{ $t("cloud.connected") }}</span>
|
||||
<span class="badge badge-primary badge-sm capitalize">{{ cloudStatus.plan.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-base-content/60">{{ $t("cloud.usage") }}</span>
|
||||
<span>
|
||||
{{ cloudStatus.usage.events_used.toLocaleString() }} /
|
||||
{{ cloudStatus.usage.events_limit.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
<progress
|
||||
class="progress w-full"
|
||||
:class="
|
||||
usagePercent > 90 ? 'progress-error' : usagePercent > 70 ? 'progress-warning' : 'progress-primary'
|
||||
"
|
||||
:value="cloudStatus.usage.events_used"
|
||||
:max="cloudStatus.usage.events_limit"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a :href="cloudUrl" target="_blank" rel="noreferrer noopener" class="btn btn-sm flex-1">
|
||||
{{ $t("cloud.dashboard") }}
|
||||
</a>
|
||||
<a :href="`${cloudUrl}/settings`" target="_blank" rel="noreferrer noopener" class="btn btn-sm flex-1">
|
||||
{{ $t("cloud.settings") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const cloudUrl = __CLOUD_URL__;
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
const cloudLinkUrl = `${cloudUrl}/link?appUrl=${encodeURIComponent(callbackUrl)}&from=cloud`;
|
||||
|
||||
const { cloudConfig, cloudStatus, cloudStatusError, isLoadingCloudStatus, fetchCloudConfig, fetchCloudStatus } =
|
||||
useCloudConfig();
|
||||
const { showToast } = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const usagePercent = computed(() => {
|
||||
if (!cloudStatus.value) return 0;
|
||||
return (cloudStatus.value.usage.events_used / cloudStatus.value.usage.events_limit) * 100;
|
||||
});
|
||||
|
||||
function onOpen() {
|
||||
if (cloudConfig.value?.linked && !cloudStatus.value && !isLoadingCloudStatus.value) {
|
||||
fetchCloudStatus();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchCloudConfig();
|
||||
|
||||
if (cloudConfig.value?.linked) {
|
||||
fetchCloudStatus();
|
||||
}
|
||||
|
||||
// Handle successful OAuth return
|
||||
if (window.location.hash === "#cloudLinked") {
|
||||
showToast(
|
||||
{
|
||||
type: "info",
|
||||
title: t("notifications.cloud-link-success.title"),
|
||||
message: t("notifications.cloud-link-success.message"),
|
||||
},
|
||||
{ expire: 6000 },
|
||||
);
|
||||
history.replaceState(null, "", window.location.pathname + window.location.search);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Not linked -->
|
||||
<template v-if="!cloudConfig">
|
||||
<div class="flex items-start gap-4">
|
||||
<mdi:cloud class="text-base-content/40 mt-0.5 text-4xl" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-base-content/70 text-sm">{{ $t("cloud.description") }}</p>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<a :href="`${cloudUrl}`" target="_blank" rel="noreferrer noopener" class="btn btn-sm">
|
||||
{{ $t("cloud.learn-more") }}
|
||||
</a>
|
||||
<a :href="cloudLinkUrl" class="btn btn-primary btn-sm">
|
||||
<mdi:link-variant class="text-base" />
|
||||
{{ $t("cloud.link-instance") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Linked -->
|
||||
<template v-else-if="cloudConfig.linked">
|
||||
<!-- Error state -->
|
||||
<div v-if="cloudStatusError" class="space-y-3">
|
||||
<div class="alert alert-error">
|
||||
<mdi:alert-circle class="text-lg" />
|
||||
<span class="text-sm">{{ $t("cloud.error") }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a :href="cloudLinkUrl" class="btn btn-primary btn-sm">
|
||||
<mdi:link-variant class="text-base" />
|
||||
{{ $t("cloud.relink-instance") }}
|
||||
</a>
|
||||
<button class="btn btn-outline btn-sm btn-error" @click="confirmUnlink">
|
||||
<mdi:link-variant-off class="text-base" />
|
||||
{{ $t("cloud.unlink") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-else-if="isLoadingCloudStatus" class="flex items-center gap-2 py-2">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
</div>
|
||||
|
||||
<!-- Healthy -->
|
||||
<div v-else-if="cloudStatus" class="space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="badge badge-success">{{ $t("cloud.connected") }}</span>
|
||||
<span class="badge badge-primary capitalize">{{ cloudStatus.plan.name }}</span>
|
||||
<span class="text-base-content/50 text-sm">{{ cloudStatus.user.email }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-base-content/60">{{ $t("cloud.usage") }}</span>
|
||||
<span>
|
||||
{{ cloudStatus.usage.events_used.toLocaleString() }} /
|
||||
{{ cloudStatus.usage.events_limit.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
<progress
|
||||
class="progress w-full max-w-xs"
|
||||
:class="usagePercent > 90 ? 'progress-error' : usagePercent > 70 ? 'progress-warning' : 'progress-primary'"
|
||||
:value="cloudStatus.usage.events_used"
|
||||
:max="cloudStatus.usage.events_limit"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a :href="cloudUrl" target="_blank" rel="noreferrer noopener" class="btn btn-sm">
|
||||
{{ $t("cloud.dashboard") }}
|
||||
</a>
|
||||
<button class="btn btn-sm btn-error" @click="confirmUnlink">
|
||||
{{ $t("cloud.unlink") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Unlink confirmation modal -->
|
||||
<dialog ref="unlinkModal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">{{ $t("cloud.unlink") }}</h3>
|
||||
<p class="py-4 text-sm">{{ $t("cloud.unlink-confirm") }}</p>
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm">{{ $t("button.cancel") }}</button>
|
||||
</form>
|
||||
<button class="btn btn-error btn-sm" :disabled="isUnlinking" @click="doUnlink">
|
||||
<span v-if="isUnlinking" class="loading loading-spinner loading-xs"></span>
|
||||
{{ $t("cloud.unlink") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button></button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const cloudUrl = __CLOUD_URL__;
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
const cloudLinkUrl = `${cloudUrl}/link?appUrl=${encodeURIComponent(callbackUrl)}&from=cloud`;
|
||||
|
||||
const {
|
||||
cloudConfig,
|
||||
cloudStatus,
|
||||
cloudStatusError,
|
||||
isLoadingCloudStatus,
|
||||
fetchCloudConfig,
|
||||
fetchCloudStatus,
|
||||
clearCloudState,
|
||||
} = useCloudConfig();
|
||||
const isUnlinking = ref(false);
|
||||
const unlinkModal = ref<HTMLDialogElement | null>(null);
|
||||
|
||||
const usagePercent = computed(() => {
|
||||
if (!cloudStatus.value) return 0;
|
||||
return (cloudStatus.value.usage.events_used / cloudStatus.value.usage.events_limit) * 100;
|
||||
});
|
||||
|
||||
function confirmUnlink() {
|
||||
unlinkModal.value?.showModal();
|
||||
}
|
||||
|
||||
async function doUnlink() {
|
||||
isUnlinking.value = true;
|
||||
try {
|
||||
const res = await fetch(withBase("/api/cloud/config"), { method: "DELETE" });
|
||||
if (!res.ok) {
|
||||
cloudStatusError.value = true;
|
||||
return;
|
||||
}
|
||||
clearCloudState();
|
||||
unlinkModal.value?.close();
|
||||
} finally {
|
||||
isUnlinking.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchCloudConfig();
|
||||
if (cloudConfig.value?.linked) {
|
||||
fetchCloudStatus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -12,6 +12,8 @@
|
||||
<mdi:bell class="size-6" />
|
||||
</router-link>
|
||||
|
||||
<CloudPopover />
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/settings' }"
|
||||
:aria-label="$t('title.settings')"
|
||||
|
||||
@@ -91,42 +91,16 @@ const { destination, close } = defineProps<{
|
||||
}>();
|
||||
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
const cloudLinkUrl = `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
const cloudLinkUrl = `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}&from=notifications`;
|
||||
const cloudSettingsUrl = `${__CLOUD_URL__}/settings`;
|
||||
|
||||
// Cloud status
|
||||
interface CloudStatus {
|
||||
user: { email: string; name: string };
|
||||
plan: { name: string; events_per_month: number; retention_days: number };
|
||||
usage: { events_used: number; events_limit: number; period: string };
|
||||
}
|
||||
|
||||
const cloudStatus = ref<CloudStatus | null>(null);
|
||||
const cloudStatusError = ref(false);
|
||||
const isLoadingCloudStatus = ref(false);
|
||||
const { cloudStatus, cloudStatusError, isLoadingCloudStatus, fetchCloudStatus } = useCloudConfig();
|
||||
|
||||
const usagePercent = computed(() => {
|
||||
if (!cloudStatus.value) return 0;
|
||||
return (cloudStatus.value.usage.events_used / cloudStatus.value.usage.events_limit) * 100;
|
||||
});
|
||||
|
||||
async function fetchCloudStatus() {
|
||||
isLoadingCloudStatus.value = true;
|
||||
cloudStatusError.value = false;
|
||||
try {
|
||||
const res = await fetch(withBase("/api/cloud/status"));
|
||||
if (!res.ok) {
|
||||
cloudStatusError.value = true;
|
||||
return;
|
||||
}
|
||||
cloudStatus.value = await res.json();
|
||||
} catch {
|
||||
cloudStatusError.value = true;
|
||||
} finally {
|
||||
isLoadingCloudStatus.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (destination?.prefix) {
|
||||
fetchCloudStatus();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<li>
|
||||
<a @click="editDestination">{{ $t("notifications.destination.edit") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="destination.type !== 'cloud'">
|
||||
<a class="text-error" @click="deleteDestination">{{ $t("notifications.destination.delete") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -2,30 +2,33 @@
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{
|
||||
isEditing
|
||||
? $t("notifications.destination-form.edit-title")
|
||||
: $t("notifications.destination-form.create-title")
|
||||
}}
|
||||
<template v-if="type === 'cloud'">
|
||||
{{ $t("notifications.destination-form.cloud-title") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
isEditing
|
||||
? $t("notifications.destination-form.edit-title")
|
||||
: $t("notifications.destination-form.create-title")
|
||||
}}
|
||||
</template>
|
||||
</h2>
|
||||
<p class="text-base-content/60">{{ $t("notifications.destination-form.description") }}</p>
|
||||
<p class="text-base-content/60">
|
||||
<template v-if="type === 'cloud'">
|
||||
{{ $t("notifications.destination-form.cloud-description") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("notifications.destination-form.description") }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Link Success Alert -->
|
||||
<div v-if="showLinkSuccess" class="alert alert-success">
|
||||
<mdi:check-circle class="text-lg" />
|
||||
<div>
|
||||
<div class="font-semibold">{{ $t("notifications.cloud-link-success.title") }}</div>
|
||||
<div class="text-sm">{{ $t("notifications.cloud-link-success.message") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type Selection -->
|
||||
<fieldset class="fieldset">
|
||||
<!-- Type Selection (only when creating) -->
|
||||
<fieldset v-if="!isEditing" class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.type") }}</legend>
|
||||
<div class="space-y-3">
|
||||
<label
|
||||
class="card card-border 20 cursor-pointer transition-colors"
|
||||
class="card card-border cursor-pointer transition-colors"
|
||||
:class="type === 'webhook' ? 'border-primary bg-primary/10' : ''"
|
||||
>
|
||||
<div class="card-body flex-row items-center gap-3 p-4">
|
||||
@@ -39,26 +42,21 @@
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
class="card card-border border-base-content/20 transition-colors"
|
||||
class="card card-border cursor-pointer transition-colors"
|
||||
:class="[
|
||||
type === 'cloud' ? 'border-primary bg-primary/10' : '',
|
||||
hasExistingCloudDestination && type !== 'cloud' ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
||||
isCloudLinked ? 'cursor-not-allowed opacity-50' : '',
|
||||
]"
|
||||
>
|
||||
<div class="card-body flex-row items-center gap-3 p-4">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="type"
|
||||
value="cloud"
|
||||
class="radio radio-primary"
|
||||
:disabled="hasExistingCloudDestination && type !== 'cloud'"
|
||||
/>
|
||||
<input type="radio" v-model="type" value="cloud" class="radio radio-primary" :disabled="isCloudLinked" />
|
||||
<div>
|
||||
<div class="font-semibold">{{ $t("notifications.destination-form.cloud-title") }}</div>
|
||||
<div class="text-base-content/60 text-sm">
|
||||
{{ $t("notifications.destination-form.cloud-description") }}
|
||||
</div>
|
||||
<div v-if="hasExistingCloudDestination && type !== 'cloud'" class="text-warning mt-1 text-xs">
|
||||
<div v-if="isCloudLinked" class="text-success mt-1 text-xs">
|
||||
<mdi:check class="inline" />
|
||||
{{ $t("notifications.destination-form.cloud-exists") }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,25 +82,17 @@ import type { Dispatcher } from "@/types/notifications";
|
||||
import WebhookDestinationForm from "./WebhookDestinationForm.vue";
|
||||
import CloudDestinationForm from "./CloudDestinationForm.vue";
|
||||
|
||||
const {
|
||||
close,
|
||||
onCreated,
|
||||
destination,
|
||||
existingDispatchers = [],
|
||||
showLinkSuccess = false,
|
||||
} = defineProps<{
|
||||
const { close, onCreated, destination } = defineProps<{
|
||||
close?: () => void;
|
||||
onCreated?: () => void;
|
||||
destination?: Dispatcher;
|
||||
existingDispatchers?: Dispatcher[];
|
||||
showLinkSuccess?: boolean;
|
||||
}>();
|
||||
|
||||
const isEditing = !!destination;
|
||||
const type = ref<"webhook" | "cloud">((destination?.type as "webhook" | "cloud") ?? "webhook");
|
||||
|
||||
const hasExistingCloudDestination = computed(() => {
|
||||
const others = isEditing ? existingDispatchers.filter((d) => d.id !== destination!.id) : existingDispatchers;
|
||||
return others.some((d) => d.type === "cloud");
|
||||
});
|
||||
const { cloudConfig, fetchCloudConfig } = useCloudConfig();
|
||||
const isCloudLinked = computed(() => !!cloudConfig.value?.linked);
|
||||
|
||||
onMounted(() => fetchCloudConfig());
|
||||
</script>
|
||||
|
||||
@@ -25,11 +25,11 @@ export function useAlertForm(options: AlertFormOptions) {
|
||||
const isEditing = computed(() => !!options.alert);
|
||||
const alertName = ref(options.alert?.name ?? options.prefill?.name ?? "");
|
||||
const containerExpression = ref(options.alert?.containerExpression ?? options.prefill?.containerExpression ?? "");
|
||||
const dispatcherId = ref(options.alert?.dispatcher?.id ?? 0);
|
||||
const dispatcherId = ref(options.alert?.dispatcher?.id ?? -1);
|
||||
const isSaving = ref(false);
|
||||
const saveError = ref<string | null>(null);
|
||||
|
||||
// Destinations
|
||||
// Destinations (cloud dispatcher with id=0 is included by the backend when configured)
|
||||
const destinations = ref<Dispatcher[]>([]);
|
||||
onMounted(async () => {
|
||||
const res = await fetch(withBase("/api/notifications/dispatchers"));
|
||||
@@ -54,7 +54,7 @@ export function useAlertForm(options: AlertFormOptions) {
|
||||
() =>
|
||||
alertName.value.trim() &&
|
||||
containerExpression.value.trim() &&
|
||||
dispatcherId.value > 0 &&
|
||||
dispatcherId.value >= 0 &&
|
||||
!containerResult.value?.error &&
|
||||
!isSaving.value,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { CloudConfig, CloudStatus } from "@/types/notifications";
|
||||
|
||||
// Shared state across all component instances
|
||||
const cloudConfig = ref<CloudConfig | null>(null);
|
||||
const cloudStatus = ref<CloudStatus | null>(null);
|
||||
const cloudStatusError = ref(false);
|
||||
const isLoadingCloudStatus = ref(false);
|
||||
|
||||
async function fetchCloudConfig() {
|
||||
try {
|
||||
const res = await fetch(withBase("/api/cloud/config"));
|
||||
if (!res.ok) {
|
||||
cloudConfig.value = null;
|
||||
return;
|
||||
}
|
||||
cloudConfig.value = await res.json();
|
||||
} catch {
|
||||
cloudConfig.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCloudStatus() {
|
||||
if (!cloudConfig.value?.linked) return;
|
||||
isLoadingCloudStatus.value = true;
|
||||
cloudStatusError.value = false;
|
||||
try {
|
||||
const res = await fetch(withBase("/api/cloud/status"));
|
||||
if (!res.ok) {
|
||||
cloudStatusError.value = true;
|
||||
return;
|
||||
}
|
||||
cloudStatus.value = await res.json();
|
||||
} catch {
|
||||
cloudStatusError.value = true;
|
||||
} finally {
|
||||
isLoadingCloudStatus.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearCloudState() {
|
||||
cloudConfig.value = null;
|
||||
cloudStatus.value = null;
|
||||
cloudStatusError.value = false;
|
||||
}
|
||||
|
||||
export function useCloudConfig() {
|
||||
return {
|
||||
cloudConfig,
|
||||
cloudStatus,
|
||||
cloudStatusError,
|
||||
isLoadingCloudStatus,
|
||||
fetchCloudConfig,
|
||||
fetchCloudStatus,
|
||||
clearCloudState,
|
||||
};
|
||||
}
|
||||
@@ -12,16 +12,9 @@
|
||||
<h3 class="text-base-content/60 mb-4 font-semibold tracking-wide uppercase">
|
||||
{{ $t("notifications.destinations") }}
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<DestinationCard
|
||||
v-for="dest in dispatchers"
|
||||
:key="dest.id"
|
||||
:destination="dest"
|
||||
:on-updated="fetchAll"
|
||||
:existing-dispatchers="dispatchers"
|
||||
class="w-full md:w-72"
|
||||
/>
|
||||
<!-- Add Destination Card -->
|
||||
|
||||
<template v-if="dispatchers.length === 0">
|
||||
<p class="text-base-content/60 mb-4 text-sm">{{ $t("notifications.empty-state.description") }}</p>
|
||||
<button
|
||||
class="card card-border border-base-content/30 hover:border-base-content/50 w-full cursor-pointer border-dashed transition-colors md:w-72"
|
||||
@click="openAddDestination"
|
||||
@@ -31,7 +24,30 @@
|
||||
<span class="text-base-content/60 text-sm">{{ $t("notifications.add-destination") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<DestinationCard
|
||||
v-for="dest in dispatchers"
|
||||
:key="dest.id"
|
||||
:destination="dest"
|
||||
:on-updated="fetchAll"
|
||||
:existing-dispatchers="dispatchers"
|
||||
class="w-full md:w-72"
|
||||
/>
|
||||
<!-- Add Destination Card -->
|
||||
<button
|
||||
class="card card-border border-base-content/30 hover:border-base-content/50 w-full cursor-pointer border-dashed transition-colors md:w-72"
|
||||
@click="openAddDestination"
|
||||
>
|
||||
<div class="card-body items-center justify-center gap-1 p-4">
|
||||
<mdi:plus class="text-2xl" />
|
||||
<span class="text-base-content/60 text-sm">{{ $t("notifications.add-destination") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Alerts Section -->
|
||||
@@ -97,26 +113,10 @@ async function fetchAll() {
|
||||
await Promise.all([fetchAlerts(), fetchDispatchers()]);
|
||||
}
|
||||
|
||||
// Handle cloudLinkSuccess hash param
|
||||
onMounted(async () => {
|
||||
await fetchAll();
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith("#cloudLinkSuccess=")) {
|
||||
const id = Number(hash.replace("#cloudLinkSuccess=", ""));
|
||||
if (!isNaN(id)) {
|
||||
const destination = dispatchers.value.find((d) => d.id === id);
|
||||
if (destination) {
|
||||
showDrawer(
|
||||
DestinationForm,
|
||||
{
|
||||
destination,
|
||||
existingDispatchers: dispatchers.value,
|
||||
showLinkSuccess: true,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (hash === "#cloudLinked") {
|
||||
router.replace({ hash: "" });
|
||||
}
|
||||
});
|
||||
@@ -142,7 +142,6 @@ function openAddDestination() {
|
||||
DestinationForm,
|
||||
{
|
||||
onCreated: fetchDispatchers,
|
||||
existingDispatchers: dispatchers.value,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="has-underline">
|
||||
<h2>{{ $t("cloud.title") }}</h2>
|
||||
</div>
|
||||
<CloudSettingsCard />
|
||||
</section>
|
||||
|
||||
<section class="@container flex flex-col">
|
||||
<div class="has-underline">
|
||||
<h2>{{ $t("settings.display") }}</h2>
|
||||
|
||||
@@ -66,3 +66,15 @@ export interface TestWebhookResult {
|
||||
statusCode?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface CloudConfig {
|
||||
prefix: string;
|
||||
expiresAt?: string;
|
||||
linked: boolean;
|
||||
}
|
||||
|
||||
export interface CloudStatus {
|
||||
user: { email: string; name: string };
|
||||
plan: { name: string; events_per_month: number; retention_days: number };
|
||||
usage: { events_used: number; events_limit: number; period: string };
|
||||
}
|
||||
|
||||
@@ -538,7 +538,6 @@ func (c *Client) Close() error {
|
||||
}
|
||||
|
||||
func (c *Client) UpdateNotificationConfig(ctx context.Context, subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
|
||||
// Convert to proto
|
||||
pbSubs := make([]*pb.NotificationSubscription, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
pbSubs[i] = &pb.NotificationSubscription{
|
||||
@@ -564,11 +563,6 @@ func (c *Client) UpdateNotificationConfig(ctx context.Context, subscriptions []t
|
||||
Url: d.URL,
|
||||
Template: d.Template,
|
||||
Headers: d.Headers,
|
||||
ApiKey: d.APIKey,
|
||||
Prefix: d.Prefix,
|
||||
}
|
||||
if d.ExpiresAt != nil {
|
||||
pbDispatchers[i].ExpiresAt = timestamppb.New(*d.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +570,21 @@ func (c *Client) UpdateNotificationConfig(ctx context.Context, subscriptions []t
|
||||
Subscriptions: pbSubs,
|
||||
Dispatchers: pbDispatchers,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) UpdateCloudConfig(ctx context.Context, cloudConfig *types.CloudConfig) error {
|
||||
req := &pb.UpdateCloudConfigRequest{}
|
||||
if cloudConfig != nil {
|
||||
req.CloudConfig = &pb.NotificationCloudConfig{
|
||||
ApiKey: cloudConfig.APIKey,
|
||||
Prefix: cloudConfig.Prefix,
|
||||
}
|
||||
if cloudConfig.ExpiresAt != nil {
|
||||
req.CloudConfig.ExpiresAt = timestamppb.New(*cloudConfig.ExpiresAt)
|
||||
}
|
||||
}
|
||||
_, err := c.client.UpdateCloudConfig(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/go-faker/faker/v4"
|
||||
@@ -35,6 +36,9 @@ func (m *mockNotificationHandler) HandleNotificationConfig(subscriptions []types
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockNotificationHandler) SetCloudDispatcher(d dispatcher.Dispatcher) {}
|
||||
func (m *mockNotificationHandler) ClearCloudDispatcher() {}
|
||||
|
||||
func (m *mockNotificationHandler) GetNotificationStats() []types.SubscriptionStats {
|
||||
return nil
|
||||
}
|
||||
|
||||
+170
-81
@@ -1478,6 +1478,86 @@ func (*UpdateNotificationConfigResponse) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_proto_rawDescGZIP(), []int{28}
|
||||
}
|
||||
|
||||
type UpdateCloudConfigRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
CloudConfig *NotificationCloudConfig `protobuf:"bytes,1,opt,name=cloudConfig,proto3" json:"cloudConfig,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UpdateCloudConfigRequest) Reset() {
|
||||
*x = UpdateCloudConfigRequest{}
|
||||
mi := &file_rpc_proto_msgTypes[29]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UpdateCloudConfigRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UpdateCloudConfigRequest) ProtoMessage() {}
|
||||
|
||||
func (x *UpdateCloudConfigRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_proto_msgTypes[29]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UpdateCloudConfigRequest.ProtoReflect.Descriptor instead.
|
||||
func (*UpdateCloudConfigRequest) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_proto_rawDescGZIP(), []int{29}
|
||||
}
|
||||
|
||||
func (x *UpdateCloudConfigRequest) GetCloudConfig() *NotificationCloudConfig {
|
||||
if x != nil {
|
||||
return x.CloudConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateCloudConfigResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UpdateCloudConfigResponse) Reset() {
|
||||
*x = UpdateCloudConfigResponse{}
|
||||
mi := &file_rpc_proto_msgTypes[30]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UpdateCloudConfigResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UpdateCloudConfigResponse) ProtoMessage() {}
|
||||
|
||||
func (x *UpdateCloudConfigResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_proto_msgTypes[30]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UpdateCloudConfigResponse.ProtoReflect.Descriptor instead.
|
||||
func (*UpdateCloudConfigResponse) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_proto_rawDescGZIP(), []int{30}
|
||||
}
|
||||
|
||||
type GetNotificationStatsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
@@ -1486,7 +1566,7 @@ type GetNotificationStatsRequest struct {
|
||||
|
||||
func (x *GetNotificationStatsRequest) Reset() {
|
||||
*x = GetNotificationStatsRequest{}
|
||||
mi := &file_rpc_proto_msgTypes[29]
|
||||
mi := &file_rpc_proto_msgTypes[31]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1498,7 +1578,7 @@ func (x *GetNotificationStatsRequest) String() string {
|
||||
func (*GetNotificationStatsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetNotificationStatsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_proto_msgTypes[29]
|
||||
mi := &file_rpc_proto_msgTypes[31]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1511,7 +1591,7 @@ func (x *GetNotificationStatsRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetNotificationStatsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetNotificationStatsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_proto_rawDescGZIP(), []int{29}
|
||||
return file_rpc_proto_rawDescGZIP(), []int{31}
|
||||
}
|
||||
|
||||
type GetNotificationStatsResponse struct {
|
||||
@@ -1523,7 +1603,7 @@ type GetNotificationStatsResponse struct {
|
||||
|
||||
func (x *GetNotificationStatsResponse) Reset() {
|
||||
*x = GetNotificationStatsResponse{}
|
||||
mi := &file_rpc_proto_msgTypes[30]
|
||||
mi := &file_rpc_proto_msgTypes[32]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1535,7 +1615,7 @@ func (x *GetNotificationStatsResponse) String() string {
|
||||
func (*GetNotificationStatsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetNotificationStatsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_proto_msgTypes[30]
|
||||
mi := &file_rpc_proto_msgTypes[32]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1548,7 +1628,7 @@ func (x *GetNotificationStatsResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetNotificationStatsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetNotificationStatsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_proto_rawDescGZIP(), []int{30}
|
||||
return file_rpc_proto_rawDescGZIP(), []int{32}
|
||||
}
|
||||
|
||||
func (x *GetNotificationStatsResponse) GetStats() []*NotificationSubscriptionStats {
|
||||
@@ -1645,11 +1725,13 @@ const file_rpc_proto_rawDesc = "" +
|
||||
"\x1fUpdateNotificationConfigRequest\x12H\n" +
|
||||
"\rsubscriptions\x18\x01 \x03(\v2\".protobuf.NotificationSubscriptionR\rsubscriptions\x12B\n" +
|
||||
"\vdispatchers\x18\x02 \x03(\v2 .protobuf.NotificationDispatcherR\vdispatchers\"\"\n" +
|
||||
" UpdateNotificationConfigResponse\"\x1d\n" +
|
||||
" UpdateNotificationConfigResponse\"_\n" +
|
||||
"\x18UpdateCloudConfigRequest\x12C\n" +
|
||||
"\vcloudConfig\x18\x01 \x01(\v2!.protobuf.NotificationCloudConfigR\vcloudConfig\"\x1b\n" +
|
||||
"\x19UpdateCloudConfigResponse\"\x1d\n" +
|
||||
"\x1bGetNotificationStatsRequest\"]\n" +
|
||||
"\x1cGetNotificationStatsResponse\x12=\n" +
|
||||
"\x05stats\x18\x01 \x03(\v2'.protobuf.NotificationSubscriptionStatsR\x05stats2\xdb\n" +
|
||||
"\n" +
|
||||
"\x05stats\x18\x01 \x03(\v2'.protobuf.NotificationSubscriptionStatsR\x05stats2\xbb\v\n" +
|
||||
"\fAgentService\x12U\n" +
|
||||
"\x0eListContainers\x12\x1f.protobuf.ListContainersRequest\x1a .protobuf.ListContainersResponse\"\x00\x12R\n" +
|
||||
"\rFindContainer\x12\x1e.protobuf.FindContainerRequest\x1a\x1f.protobuf.FindContainerResponse\"\x00\x12K\n" +
|
||||
@@ -1665,7 +1747,8 @@ const file_rpc_proto_rawDesc = "" +
|
||||
"\x0fUpdateContainer\x12 .protobuf.UpdateContainerRequest\x1a!.protobuf.UpdateContainerProgress\"\x000\x01\x12V\n" +
|
||||
"\rContainerExec\x12\x1e.protobuf.ContainerExecRequest\x1a\x1f.protobuf.ContainerExecResponse\"\x00(\x010\x01\x12\\\n" +
|
||||
"\x0fContainerAttach\x12 .protobuf.ContainerAttachRequest\x1a!.protobuf.ContainerAttachResponse\"\x00(\x010\x01\x12s\n" +
|
||||
"\x18UpdateNotificationConfig\x12).protobuf.UpdateNotificationConfigRequest\x1a*.protobuf.UpdateNotificationConfigResponse\"\x00\x12g\n" +
|
||||
"\x18UpdateNotificationConfig\x12).protobuf.UpdateNotificationConfigRequest\x1a*.protobuf.UpdateNotificationConfigResponse\"\x00\x12^\n" +
|
||||
"\x11UpdateCloudConfig\x12\".protobuf.UpdateCloudConfigRequest\x1a#.protobuf.UpdateCloudConfigResponse\"\x00\x12g\n" +
|
||||
"\x14GetNotificationStats\x12%.protobuf.GetNotificationStatsRequest\x1a&.protobuf.GetNotificationStatsResponse\"\x00B\x13Z\x11internal/agent/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -1680,7 +1763,7 @@ func file_rpc_proto_rawDescGZIP() []byte {
|
||||
return file_rpc_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 33)
|
||||
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 35)
|
||||
var file_rpc_proto_goTypes = []any{
|
||||
(*ListContainersRequest)(nil), // 0: protobuf.ListContainersRequest
|
||||
(*RepeatedString)(nil), // 1: protobuf.RepeatedString
|
||||
@@ -1711,79 +1794,85 @@ var file_rpc_proto_goTypes = []any{
|
||||
(*ContainerAttachResponse)(nil), // 26: protobuf.ContainerAttachResponse
|
||||
(*UpdateNotificationConfigRequest)(nil), // 27: protobuf.UpdateNotificationConfigRequest
|
||||
(*UpdateNotificationConfigResponse)(nil), // 28: protobuf.UpdateNotificationConfigResponse
|
||||
(*GetNotificationStatsRequest)(nil), // 29: protobuf.GetNotificationStatsRequest
|
||||
(*GetNotificationStatsResponse)(nil), // 30: protobuf.GetNotificationStatsResponse
|
||||
nil, // 31: protobuf.ListContainersRequest.FilterEntry
|
||||
nil, // 32: protobuf.FindContainerRequest.FilterEntry
|
||||
(*Container)(nil), // 33: protobuf.Container
|
||||
(*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp
|
||||
(*LogEvent)(nil), // 35: protobuf.LogEvent
|
||||
(*ContainerEvent)(nil), // 36: protobuf.ContainerEvent
|
||||
(*ContainerStat)(nil), // 37: protobuf.ContainerStat
|
||||
(*Host)(nil), // 38: protobuf.Host
|
||||
(ContainerAction)(0), // 39: protobuf.ContainerAction
|
||||
(*NotificationSubscription)(nil), // 40: protobuf.NotificationSubscription
|
||||
(*NotificationDispatcher)(nil), // 41: protobuf.NotificationDispatcher
|
||||
(*NotificationSubscriptionStats)(nil), // 42: protobuf.NotificationSubscriptionStats
|
||||
(*UpdateCloudConfigRequest)(nil), // 29: protobuf.UpdateCloudConfigRequest
|
||||
(*UpdateCloudConfigResponse)(nil), // 30: protobuf.UpdateCloudConfigResponse
|
||||
(*GetNotificationStatsRequest)(nil), // 31: protobuf.GetNotificationStatsRequest
|
||||
(*GetNotificationStatsResponse)(nil), // 32: protobuf.GetNotificationStatsResponse
|
||||
nil, // 33: protobuf.ListContainersRequest.FilterEntry
|
||||
nil, // 34: protobuf.FindContainerRequest.FilterEntry
|
||||
(*Container)(nil), // 35: protobuf.Container
|
||||
(*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp
|
||||
(*LogEvent)(nil), // 37: protobuf.LogEvent
|
||||
(*ContainerEvent)(nil), // 38: protobuf.ContainerEvent
|
||||
(*ContainerStat)(nil), // 39: protobuf.ContainerStat
|
||||
(*Host)(nil), // 40: protobuf.Host
|
||||
(ContainerAction)(0), // 41: protobuf.ContainerAction
|
||||
(*NotificationSubscription)(nil), // 42: protobuf.NotificationSubscription
|
||||
(*NotificationDispatcher)(nil), // 43: protobuf.NotificationDispatcher
|
||||
(*NotificationCloudConfig)(nil), // 44: protobuf.NotificationCloudConfig
|
||||
(*NotificationSubscriptionStats)(nil), // 45: protobuf.NotificationSubscriptionStats
|
||||
}
|
||||
var file_rpc_proto_depIdxs = []int32{
|
||||
31, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
|
||||
33, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
|
||||
32, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
|
||||
33, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
|
||||
34, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
|
||||
35, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
|
||||
34, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
34, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
34, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
34, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
36, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
|
||||
37, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
|
||||
38, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
|
||||
33, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
|
||||
39, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
|
||||
33, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
|
||||
35, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
|
||||
34, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
|
||||
35, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
|
||||
36, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
|
||||
37, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
|
||||
36, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
36, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
36, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
36, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
38, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
|
||||
39, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
|
||||
40, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
|
||||
35, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
|
||||
41, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
|
||||
23, // 15: protobuf.ContainerExecRequest.resize:type_name -> protobuf.ResizePayload
|
||||
23, // 16: protobuf.ContainerAttachRequest.resize:type_name -> protobuf.ResizePayload
|
||||
40, // 17: protobuf.UpdateNotificationConfigRequest.subscriptions:type_name -> protobuf.NotificationSubscription
|
||||
41, // 18: protobuf.UpdateNotificationConfigRequest.dispatchers:type_name -> protobuf.NotificationDispatcher
|
||||
42, // 19: protobuf.GetNotificationStatsResponse.stats:type_name -> protobuf.NotificationSubscriptionStats
|
||||
1, // 20: protobuf.ListContainersRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
1, // 21: protobuf.FindContainerRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
0, // 22: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
|
||||
3, // 23: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
|
||||
5, // 24: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
|
||||
7, // 25: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
|
||||
8, // 26: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
|
||||
10, // 27: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
|
||||
12, // 28: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
|
||||
16, // 29: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
|
||||
14, // 30: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
|
||||
18, // 31: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
|
||||
20, // 32: protobuf.AgentService.UpdateContainer:input_type -> protobuf.UpdateContainerRequest
|
||||
22, // 33: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
|
||||
25, // 34: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
|
||||
27, // 35: protobuf.AgentService.UpdateNotificationConfig:input_type -> protobuf.UpdateNotificationConfigRequest
|
||||
29, // 36: protobuf.AgentService.GetNotificationStats:input_type -> protobuf.GetNotificationStatsRequest
|
||||
2, // 37: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
|
||||
4, // 38: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
|
||||
6, // 39: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
|
||||
6, // 40: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
|
||||
9, // 41: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
|
||||
11, // 42: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
|
||||
13, // 43: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
|
||||
17, // 44: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
|
||||
15, // 45: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
|
||||
19, // 46: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
|
||||
21, // 47: protobuf.AgentService.UpdateContainer:output_type -> protobuf.UpdateContainerProgress
|
||||
24, // 48: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
|
||||
26, // 49: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
|
||||
28, // 50: protobuf.AgentService.UpdateNotificationConfig:output_type -> protobuf.UpdateNotificationConfigResponse
|
||||
30, // 51: protobuf.AgentService.GetNotificationStats:output_type -> protobuf.GetNotificationStatsResponse
|
||||
37, // [37:52] is the sub-list for method output_type
|
||||
22, // [22:37] is the sub-list for method input_type
|
||||
22, // [22:22] is the sub-list for extension type_name
|
||||
22, // [22:22] is the sub-list for extension extendee
|
||||
0, // [0:22] is the sub-list for field type_name
|
||||
42, // 17: protobuf.UpdateNotificationConfigRequest.subscriptions:type_name -> protobuf.NotificationSubscription
|
||||
43, // 18: protobuf.UpdateNotificationConfigRequest.dispatchers:type_name -> protobuf.NotificationDispatcher
|
||||
44, // 19: protobuf.UpdateCloudConfigRequest.cloudConfig:type_name -> protobuf.NotificationCloudConfig
|
||||
45, // 20: protobuf.GetNotificationStatsResponse.stats:type_name -> protobuf.NotificationSubscriptionStats
|
||||
1, // 21: protobuf.ListContainersRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
1, // 22: protobuf.FindContainerRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
0, // 23: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
|
||||
3, // 24: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
|
||||
5, // 25: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
|
||||
7, // 26: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
|
||||
8, // 27: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
|
||||
10, // 28: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
|
||||
12, // 29: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
|
||||
16, // 30: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
|
||||
14, // 31: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
|
||||
18, // 32: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
|
||||
20, // 33: protobuf.AgentService.UpdateContainer:input_type -> protobuf.UpdateContainerRequest
|
||||
22, // 34: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
|
||||
25, // 35: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
|
||||
27, // 36: protobuf.AgentService.UpdateNotificationConfig:input_type -> protobuf.UpdateNotificationConfigRequest
|
||||
29, // 37: protobuf.AgentService.UpdateCloudConfig:input_type -> protobuf.UpdateCloudConfigRequest
|
||||
31, // 38: protobuf.AgentService.GetNotificationStats:input_type -> protobuf.GetNotificationStatsRequest
|
||||
2, // 39: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
|
||||
4, // 40: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
|
||||
6, // 41: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
|
||||
6, // 42: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
|
||||
9, // 43: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
|
||||
11, // 44: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
|
||||
13, // 45: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
|
||||
17, // 46: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
|
||||
15, // 47: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
|
||||
19, // 48: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
|
||||
21, // 49: protobuf.AgentService.UpdateContainer:output_type -> protobuf.UpdateContainerProgress
|
||||
24, // 50: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
|
||||
26, // 51: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
|
||||
28, // 52: protobuf.AgentService.UpdateNotificationConfig:output_type -> protobuf.UpdateNotificationConfigResponse
|
||||
30, // 53: protobuf.AgentService.UpdateCloudConfig:output_type -> protobuf.UpdateCloudConfigResponse
|
||||
32, // 54: protobuf.AgentService.GetNotificationStats:output_type -> protobuf.GetNotificationStatsResponse
|
||||
39, // [39:55] is the sub-list for method output_type
|
||||
23, // [23:39] is the sub-list for method input_type
|
||||
23, // [23:23] is the sub-list for extension type_name
|
||||
23, // [23:23] is the sub-list for extension extendee
|
||||
0, // [0:23] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_rpc_proto_init() }
|
||||
@@ -1806,7 +1895,7 @@ func file_rpc_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_proto_rawDesc), len(file_rpc_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 33,
|
||||
NumMessages: 35,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
AgentService_ContainerExec_FullMethodName = "/protobuf.AgentService/ContainerExec"
|
||||
AgentService_ContainerAttach_FullMethodName = "/protobuf.AgentService/ContainerAttach"
|
||||
AgentService_UpdateNotificationConfig_FullMethodName = "/protobuf.AgentService/UpdateNotificationConfig"
|
||||
AgentService_UpdateCloudConfig_FullMethodName = "/protobuf.AgentService/UpdateCloudConfig"
|
||||
AgentService_GetNotificationStats_FullMethodName = "/protobuf.AgentService/GetNotificationStats"
|
||||
)
|
||||
|
||||
@@ -54,6 +55,7 @@ type AgentServiceClient interface {
|
||||
ContainerExec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerExecRequest, ContainerExecResponse], error)
|
||||
ContainerAttach(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerAttachRequest, ContainerAttachResponse], error)
|
||||
UpdateNotificationConfig(ctx context.Context, in *UpdateNotificationConfigRequest, opts ...grpc.CallOption) (*UpdateNotificationConfigResponse, error)
|
||||
UpdateCloudConfig(ctx context.Context, in *UpdateCloudConfigRequest, opts ...grpc.CallOption) (*UpdateCloudConfigResponse, error)
|
||||
GetNotificationStats(ctx context.Context, in *GetNotificationStatsRequest, opts ...grpc.CallOption) (*GetNotificationStatsResponse, error)
|
||||
}
|
||||
|
||||
@@ -274,6 +276,16 @@ func (c *agentServiceClient) UpdateNotificationConfig(ctx context.Context, in *U
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *agentServiceClient) UpdateCloudConfig(ctx context.Context, in *UpdateCloudConfigRequest, opts ...grpc.CallOption) (*UpdateCloudConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(UpdateCloudConfigResponse)
|
||||
err := c.cc.Invoke(ctx, AgentService_UpdateCloudConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *agentServiceClient) GetNotificationStats(ctx context.Context, in *GetNotificationStatsRequest, opts ...grpc.CallOption) (*GetNotificationStatsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetNotificationStatsResponse)
|
||||
@@ -302,6 +314,7 @@ type AgentServiceServer interface {
|
||||
ContainerExec(grpc.BidiStreamingServer[ContainerExecRequest, ContainerExecResponse]) error
|
||||
ContainerAttach(grpc.BidiStreamingServer[ContainerAttachRequest, ContainerAttachResponse]) error
|
||||
UpdateNotificationConfig(context.Context, *UpdateNotificationConfigRequest) (*UpdateNotificationConfigResponse, error)
|
||||
UpdateCloudConfig(context.Context, *UpdateCloudConfigRequest) (*UpdateCloudConfigResponse, error)
|
||||
GetNotificationStats(context.Context, *GetNotificationStatsRequest) (*GetNotificationStatsResponse, error)
|
||||
mustEmbedUnimplementedAgentServiceServer()
|
||||
}
|
||||
@@ -355,6 +368,9 @@ func (UnimplementedAgentServiceServer) ContainerAttach(grpc.BidiStreamingServer[
|
||||
func (UnimplementedAgentServiceServer) UpdateNotificationConfig(context.Context, *UpdateNotificationConfigRequest) (*UpdateNotificationConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateNotificationConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) UpdateCloudConfig(context.Context, *UpdateCloudConfigRequest) (*UpdateCloudConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateCloudConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) GetNotificationStats(context.Context, *GetNotificationStatsRequest) (*GetNotificationStatsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetNotificationStats not implemented")
|
||||
}
|
||||
@@ -560,6 +576,24 @@ func _AgentService_UpdateNotificationConfig_Handler(srv interface{}, ctx context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AgentService_UpdateCloudConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpdateCloudConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AgentServiceServer).UpdateCloudConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AgentService_UpdateCloudConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AgentServiceServer).UpdateCloudConfig(ctx, req.(*UpdateCloudConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AgentService_GetNotificationStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetNotificationStatsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -605,6 +639,10 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "UpdateNotificationConfig",
|
||||
Handler: _AgentService_UpdateNotificationConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateCloudConfig",
|
||||
Handler: _AgentService_UpdateCloudConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetNotificationStats",
|
||||
Handler: _AgentService_GetNotificationStats_Handler,
|
||||
|
||||
@@ -992,9 +992,6 @@ type NotificationDispatcher struct {
|
||||
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Template string `protobuf:"bytes,5,opt,name=template,proto3" json:"template,omitempty"`
|
||||
Headers map[string]string `protobuf:"bytes,6,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
ApiKey string `protobuf:"bytes,7,opt,name=apiKey,proto3" json:"apiKey,omitempty"`
|
||||
Prefix string `protobuf:"bytes,8,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1071,21 +1068,60 @@ func (x *NotificationDispatcher) GetHeaders() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetApiKey() string {
|
||||
type NotificationCloudConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ApiKey string `protobuf:"bytes,1,opt,name=apiKey,proto3" json:"apiKey,omitempty"`
|
||||
Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *NotificationCloudConfig) Reset() {
|
||||
*x = NotificationCloudConfig{}
|
||||
mi := &file_types_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *NotificationCloudConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*NotificationCloudConfig) ProtoMessage() {}
|
||||
|
||||
func (x *NotificationCloudConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use NotificationCloudConfig.ProtoReflect.Descriptor instead.
|
||||
func (*NotificationCloudConfig) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *NotificationCloudConfig) GetApiKey() string {
|
||||
if x != nil {
|
||||
return x.ApiKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetPrefix() string {
|
||||
func (x *NotificationCloudConfig) GetPrefix() string {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetExpiresAt() *timestamppb.Timestamp {
|
||||
func (x *NotificationCloudConfig) GetExpiresAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.ExpiresAt
|
||||
}
|
||||
@@ -1104,7 +1140,7 @@ type NotificationSubscriptionStats struct {
|
||||
|
||||
func (x *NotificationSubscriptionStats) Reset() {
|
||||
*x = NotificationSubscriptionStats{}
|
||||
mi := &file_types_proto_msgTypes[11]
|
||||
mi := &file_types_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1116,7 +1152,7 @@ func (x *NotificationSubscriptionStats) String() string {
|
||||
func (*NotificationSubscriptionStats) ProtoMessage() {}
|
||||
|
||||
func (x *NotificationSubscriptionStats) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[11]
|
||||
mi := &file_types_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1129,7 +1165,7 @@ func (x *NotificationSubscriptionStats) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use NotificationSubscriptionStats.ProtoReflect.Descriptor instead.
|
||||
func (*NotificationSubscriptionStats) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{11}
|
||||
return file_types_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *NotificationSubscriptionStats) GetSubscriptionId() int32 {
|
||||
@@ -1259,20 +1295,22 @@ const file_types_proto_rawDesc = "" +
|
||||
"\bcooldown\x18\b \x01(\x05R\bcooldown\x12\"\n" +
|
||||
"\fsampleWindow\x18\t \x01(\x05R\fsampleWindow\x12(\n" +
|
||||
"\x0feventExpression\x18\n" +
|
||||
" \x01(\tR\x0feventExpression\"\xed\x02\n" +
|
||||
" \x01(\tR\x0feventExpression\"\x95\x02\n" +
|
||||
"\x16NotificationDispatcher\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04type\x18\x03 \x01(\tR\x04type\x12\x10\n" +
|
||||
"\x03url\x18\x04 \x01(\tR\x03url\x12\x1a\n" +
|
||||
"\btemplate\x18\x05 \x01(\tR\btemplate\x12G\n" +
|
||||
"\aheaders\x18\x06 \x03(\v2-.protobuf.NotificationDispatcher.HeadersEntryR\aheaders\x12\x16\n" +
|
||||
"\x06apiKey\x18\a \x01(\tR\x06apiKey\x12\x16\n" +
|
||||
"\x06prefix\x18\b \x01(\tR\x06prefix\x128\n" +
|
||||
"\texpiresAt\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x1a:\n" +
|
||||
"\aheaders\x18\x06 \x03(\v2-.protobuf.NotificationDispatcher.HeadersEntryR\aheaders\x1a:\n" +
|
||||
"\fHeadersEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe7\x01\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\a\x10\bJ\x04\b\b\x10\tJ\x04\b\t\x10\n" +
|
||||
"\"\x83\x01\n" +
|
||||
"\x17NotificationCloudConfig\x12\x16\n" +
|
||||
"\x06apiKey\x18\x01 \x01(\tR\x06apiKey\x12\x16\n" +
|
||||
"\x06prefix\x18\x02 \x01(\tR\x06prefix\x128\n" +
|
||||
"\texpiresAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\xe7\x01\n" +
|
||||
"\x1dNotificationSubscriptionStats\x12&\n" +
|
||||
"\x0esubscriptionId\x18\x01 \x01(\x05R\x0esubscriptionId\x12\"\n" +
|
||||
"\ftriggerCount\x18\x02 \x01(\x03R\ftriggerCount\x12D\n" +
|
||||
@@ -1296,7 +1334,7 @@ func file_types_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_types_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
|
||||
var file_types_proto_goTypes = []any{
|
||||
(ContainerAction)(0), // 0: protobuf.ContainerAction
|
||||
(*Container)(nil), // 1: protobuf.Container
|
||||
@@ -1310,29 +1348,30 @@ var file_types_proto_goTypes = []any{
|
||||
(*Host)(nil), // 9: protobuf.Host
|
||||
(*NotificationSubscription)(nil), // 10: protobuf.NotificationSubscription
|
||||
(*NotificationDispatcher)(nil), // 11: protobuf.NotificationDispatcher
|
||||
(*NotificationSubscriptionStats)(nil), // 12: protobuf.NotificationSubscriptionStats
|
||||
nil, // 13: protobuf.Container.LabelsEntry
|
||||
nil, // 14: protobuf.ContainerEvent.ActorAttributesEntry
|
||||
nil, // 15: protobuf.Host.LabelsEntry
|
||||
nil, // 16: protobuf.NotificationDispatcher.HeadersEntry
|
||||
(*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 18: google.protobuf.Any
|
||||
(*NotificationCloudConfig)(nil), // 12: protobuf.NotificationCloudConfig
|
||||
(*NotificationSubscriptionStats)(nil), // 13: protobuf.NotificationSubscriptionStats
|
||||
nil, // 14: protobuf.Container.LabelsEntry
|
||||
nil, // 15: protobuf.ContainerEvent.ActorAttributesEntry
|
||||
nil, // 16: protobuf.Host.LabelsEntry
|
||||
nil, // 17: protobuf.NotificationDispatcher.HeadersEntry
|
||||
(*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 19: google.protobuf.Any
|
||||
}
|
||||
var file_types_proto_depIdxs = []int32{
|
||||
17, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
|
||||
17, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
|
||||
13, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
|
||||
18, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
|
||||
18, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
|
||||
14, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
|
||||
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
|
||||
17, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
|
||||
18, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
|
||||
17, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
18, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
|
||||
19, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
|
||||
18, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
3, // 7: protobuf.GroupMessage.fragments:type_name -> protobuf.LogFragment
|
||||
17, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
14, // 9: protobuf.ContainerEvent.actorAttributes:type_name -> protobuf.ContainerEvent.ActorAttributesEntry
|
||||
15, // 10: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
|
||||
16, // 11: protobuf.NotificationDispatcher.headers:type_name -> protobuf.NotificationDispatcher.HeadersEntry
|
||||
17, // 12: protobuf.NotificationDispatcher.expiresAt:type_name -> google.protobuf.Timestamp
|
||||
17, // 13: protobuf.NotificationSubscriptionStats.lastTriggeredAt:type_name -> google.protobuf.Timestamp
|
||||
18, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
15, // 9: protobuf.ContainerEvent.actorAttributes:type_name -> protobuf.ContainerEvent.ActorAttributesEntry
|
||||
16, // 10: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
|
||||
17, // 11: protobuf.NotificationDispatcher.headers:type_name -> protobuf.NotificationDispatcher.HeadersEntry
|
||||
18, // 12: protobuf.NotificationCloudConfig.expiresAt:type_name -> google.protobuf.Timestamp
|
||||
18, // 13: protobuf.NotificationSubscriptionStats.lastTriggeredAt:type_name -> google.protobuf.Timestamp
|
||||
14, // [14:14] is the sub-list for method output_type
|
||||
14, // [14:14] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
@@ -1351,7 +1390,7 @@ func file_types_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 16,
|
||||
NumMessages: 17,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent/pb"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
@@ -30,6 +31,8 @@ import (
|
||||
// NotificationConfigHandler handles notification config updates received from the main server
|
||||
type NotificationConfigHandler interface {
|
||||
HandleNotificationConfig(subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error
|
||||
SetCloudDispatcher(d dispatcher.Dispatcher)
|
||||
ClearCloudDispatcher()
|
||||
GetNotificationStats() []types.SubscriptionStats
|
||||
}
|
||||
|
||||
@@ -462,12 +465,6 @@ func (s *server) UpdateNotificationConfig(ctx context.Context, req *pb.UpdateNot
|
||||
URL: d.Url,
|
||||
Template: d.Template,
|
||||
Headers: d.Headers,
|
||||
APIKey: d.ApiKey,
|
||||
Prefix: d.Prefix,
|
||||
}
|
||||
if d.ExpiresAt != nil {
|
||||
t := d.ExpiresAt.AsTime()
|
||||
dispatchers[i].ExpiresAt = &t
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +478,27 @@ func (s *server) UpdateNotificationConfig(ctx context.Context, req *pb.UpdateNot
|
||||
return &pb.UpdateNotificationConfigResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *server) UpdateCloudConfig(ctx context.Context, req *pb.UpdateCloudConfigRequest) (*pb.UpdateCloudConfigResponse, error) {
|
||||
if cc := req.CloudConfig; cc != nil && cc.ApiKey != "" {
|
||||
var expiresAt *time.Time
|
||||
if cc.ExpiresAt != nil {
|
||||
t := cc.ExpiresAt.AsTime()
|
||||
expiresAt = &t
|
||||
}
|
||||
d, err := dispatcher.NewCloudDispatcher("Dozzle Cloud", cc.ApiKey, cc.Prefix, expiresAt)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create cloud dispatcher from broadcast config")
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
s.notificationConfigHandler.SetCloudDispatcher(d)
|
||||
log.Info().Msg("Updated cloud config from main server")
|
||||
} else {
|
||||
s.notificationConfigHandler.ClearCloudDispatcher()
|
||||
log.Info().Msg("Cleared cloud config from main server")
|
||||
}
|
||||
return &pb.UpdateCloudConfigResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *server) GetNotificationStats(ctx context.Context, req *pb.GetNotificationStatsRequest) (*pb.GetNotificationStatsResponse, error) {
|
||||
stats := s.notificationConfigHandler.GetNotificationStats()
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Package migration contains one-time data migrations that run at startup.
|
||||
// Delete this package once all users have migrated.
|
||||
package migration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
type notificationConfig struct {
|
||||
Subscriptions []subscription `yaml:"subscriptions"`
|
||||
Dispatchers []dispatcher `yaml:"dispatchers"`
|
||||
}
|
||||
|
||||
type subscription struct {
|
||||
ID int `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
DispatcherID int `yaml:"dispatcherId"`
|
||||
LogExpression string `yaml:"logExpression"`
|
||||
ContainerExpression string `yaml:"containerExpression"`
|
||||
MetricExpression string `yaml:"metricExpression,omitempty"`
|
||||
EventExpression string `yaml:"eventExpression,omitempty"`
|
||||
Cooldown int `yaml:"cooldown,omitempty"`
|
||||
SampleWindow int `yaml:"sampleWindow,omitempty"`
|
||||
}
|
||||
|
||||
type dispatcher struct {
|
||||
ID int `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Template string `yaml:"template,omitempty"`
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
APIKey string `yaml:"apiKey,omitempty"`
|
||||
Prefix string `yaml:"prefix,omitempty"`
|
||||
ExpiresAt *time.Time `yaml:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
type cloudConfig struct {
|
||||
APIKey string `yaml:"apiKey"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
ExpiresAt *time.Time `yaml:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
// MigrateCloudConfig splits the old notifications.yml (which embedded cloud credentials
|
||||
// in the dispatchers list) into two files: a clean notifications.yml and a new cloud.yml.
|
||||
// Subscriptions are remapped from the old cloud dispatcher ID to 0.
|
||||
// No-op if cloud.yml already exists or there is no cloud dispatcher to migrate.
|
||||
func MigrateCloudConfig(notificationsPath, cloudPath string) {
|
||||
if fileExists(cloudPath) || !fileExists(notificationsPath) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(notificationsPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var config notificationConfig
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Find cloud dispatcher
|
||||
var cloud *cloudConfig
|
||||
var cloudID int
|
||||
remaining := make([]dispatcher, 0, len(config.Dispatchers))
|
||||
for _, d := range config.Dispatchers {
|
||||
if d.Type == "cloud" && d.APIKey != "" {
|
||||
cloud = &cloudConfig{APIKey: d.APIKey, Prefix: d.Prefix, ExpiresAt: d.ExpiresAt}
|
||||
cloudID = d.ID
|
||||
} else {
|
||||
remaining = append(remaining, d)
|
||||
}
|
||||
}
|
||||
|
||||
if cloud == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Int("oldDispatcherId", cloudID).Msg("Migrating cloud config from notifications.yml to cloud.yml")
|
||||
|
||||
// Remap subscriptions from old cloud dispatcher ID to 0
|
||||
for i := range config.Subscriptions {
|
||||
if config.Subscriptions[i].DispatcherID == cloudID {
|
||||
config.Subscriptions[i].DispatcherID = 0
|
||||
}
|
||||
}
|
||||
config.Dispatchers = remaining
|
||||
|
||||
// Write cloud.yml
|
||||
if err := writeYAML(cloudPath, cloud); err != nil {
|
||||
log.Error().Err(err).Msg("Could not write cloud.yml")
|
||||
return
|
||||
}
|
||||
|
||||
// Rewrite notifications.yml
|
||||
if err := writeYAML(notificationsPath, config); err != nil {
|
||||
log.Error().Err(err).Msg("Could not rewrite notifications.yml")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("Migration complete: created cloud.yml and updated notifications.yml")
|
||||
}
|
||||
|
||||
func writeYAML(path string, v any) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
encoder := yaml.NewEncoder(file)
|
||||
defer encoder.Close()
|
||||
return encoder.Encode(v)
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// CloudConfig holds the cloud dispatcher credentials and metadata.
|
||||
type CloudConfig struct {
|
||||
APIKey string `yaml:"apiKey"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
ExpiresAt *time.Time `yaml:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
// WriteCloudConfig encodes the given CloudConfig to the writer in YAML format.
|
||||
func WriteCloudConfig(w io.Writer, config CloudConfig) error {
|
||||
encoder := yaml.NewEncoder(w)
|
||||
defer encoder.Close()
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
|
||||
// LoadCloudConfig decodes a CloudConfig from the reader.
|
||||
func LoadCloudConfig(r io.Reader) (CloudConfig, error) {
|
||||
var config CloudConfig
|
||||
decoder := yaml.NewDecoder(r)
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
return CloudConfig{}, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -13,11 +13,20 @@ import (
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// WriteConfig writes the current configuration to a writer in YAML format
|
||||
// WriteConfig writes the current configuration to a writer in YAML format.
|
||||
// Cloud dispatchers are excluded because they are persisted separately in cloud.yml.
|
||||
func (m *Manager) WriteConfig(w io.Writer) error {
|
||||
allDispatchers := m.Dispatchers()
|
||||
dispatchers := make([]DispatcherConfig, 0, len(allDispatchers))
|
||||
for _, d := range allDispatchers {
|
||||
if d.Type != "cloud" {
|
||||
dispatchers = append(dispatchers, d)
|
||||
}
|
||||
}
|
||||
|
||||
config := Config{
|
||||
Subscriptions: m.Subscriptions(),
|
||||
Dispatchers: m.Dispatchers(),
|
||||
Dispatchers: dispatchers,
|
||||
}
|
||||
|
||||
encoder := yaml.NewEncoder(w)
|
||||
@@ -55,15 +64,12 @@ func (m *Manager) LoadConfig(r io.Reader) error {
|
||||
dispatchers := make([]types.DispatcherConfig, len(config.Dispatchers))
|
||||
for i, d := range config.Dispatchers {
|
||||
dispatchers[i] = types.DispatcherConfig{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
Headers: d.Headers,
|
||||
APIKey: d.APIKey,
|
||||
Prefix: d.Prefix,
|
||||
ExpiresAt: d.ExpiresAt,
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
Headers: d.Headers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,21 +168,22 @@ func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionCon
|
||||
}
|
||||
}
|
||||
|
||||
// Load dispatchers
|
||||
// Load dispatchers (cloud dispatchers are skipped; they are managed via cloud.yml)
|
||||
for _, dc := range dispatchers {
|
||||
if dc.Type == "cloud" {
|
||||
continue
|
||||
}
|
||||
d, err := createDispatcher(DispatcherConfig{
|
||||
ID: dc.ID,
|
||||
Name: dc.Name,
|
||||
Type: dc.Type,
|
||||
URL: dc.URL,
|
||||
Template: dc.Template,
|
||||
Headers: dc.Headers,
|
||||
APIKey: dc.APIKey,
|
||||
Prefix: dc.Prefix,
|
||||
ExpiresAt: dc.ExpiresAt,
|
||||
ID: dc.ID,
|
||||
Name: dc.Name,
|
||||
Type: dc.Type,
|
||||
URL: dc.URL,
|
||||
Template: dc.Template,
|
||||
Headers: dc.Headers,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create dispatcher %s: %w", dc.Name, err)
|
||||
log.Warn().Err(err).Str("name", dc.Name).Str("type", dc.Type).Msg("Skipping unknown dispatcher type")
|
||||
continue
|
||||
}
|
||||
m.dispatchers.Store(dc.ID, d)
|
||||
log.Debug().Int("id", dc.ID).Msg("Loaded dispatcher from state sync")
|
||||
@@ -188,13 +195,12 @@ func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionCon
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDispatcher creates a dispatcher from a DispatcherConfig
|
||||
// createDispatcher creates a dispatcher from a DispatcherConfig.
|
||||
// Cloud dispatchers are not created here; they are managed via cloud.yml and SetCloudDispatcher.
|
||||
func createDispatcher(config DispatcherConfig) (dispatcher.Dispatcher, error) {
|
||||
switch config.Type {
|
||||
case "webhook":
|
||||
return dispatcher.NewWebhookDispatcher(config.Name, config.URL, config.Template, config.Headers)
|
||||
case "cloud":
|
||||
return dispatcher.NewCloudDispatcher(config.Name, config.APIKey, config.Prefix, config.ExpiresAt)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown dispatcher type: %s", config.Type)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func (l *ContainerLogListener) startListening(c container.Container, client cont
|
||||
go func() {
|
||||
defer l.cleanupStream(c.ID, streamCtx)
|
||||
log.Debug().Str("containerID", c.ID).Str("name", c.Name).Msg("Started listening to container")
|
||||
if err := client.StreamLogs(streamCtx, c, since, container.STDALL, l.logChannel); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) {
|
||||
if err := client.StreamLogs(streamCtx, c, since, container.STDALL, l.logChannel); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) && streamCtx.Err() == nil {
|
||||
log.Error().Err(err).Str("containerID", c.ID).Msg("Error streaming logs")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
type Manager struct {
|
||||
subscriptions *xsync.Map[int, *Subscription]
|
||||
dispatchers *xsync.Map[int, dispatcher.Dispatcher]
|
||||
cloudDispatcher atomic.Pointer[dispatcher.Dispatcher]
|
||||
subscriptionCounter atomic.Int32
|
||||
dispatcherCounter atomic.Int32
|
||||
listener *ContainerLogListener
|
||||
@@ -322,6 +323,31 @@ func (m *Manager) RemoveDispatcher(id int) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetCloudDispatcher sets the dedicated cloud dispatcher used for subscriptions with DispatcherID == 0.
|
||||
func (m *Manager) SetCloudDispatcher(d dispatcher.Dispatcher) {
|
||||
m.cloudDispatcher.Store(&d)
|
||||
log.Debug().Msg("Set cloud dispatcher")
|
||||
}
|
||||
|
||||
// ClearCloudDispatcher removes the cloud dispatcher.
|
||||
func (m *Manager) ClearCloudDispatcher() {
|
||||
m.cloudDispatcher.Store(nil)
|
||||
log.Debug().Msg("Cleared cloud dispatcher")
|
||||
}
|
||||
|
||||
|
||||
// getDispatcher resolves a dispatcher by subscription's DispatcherID.
|
||||
// DispatcherID == 0 means the cloud dispatcher; otherwise lookup in the dispatchers map.
|
||||
func (m *Manager) getDispatcher(id int) (dispatcher.Dispatcher, bool) {
|
||||
if id == 0 {
|
||||
if p := m.cloudDispatcher.Load(); p != nil {
|
||||
return *p, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
return m.dispatchers.Load(id)
|
||||
}
|
||||
|
||||
// Subscriptions returns all subscriptions sorted by ID
|
||||
func (m *Manager) Subscriptions() []*Subscription {
|
||||
result := make([]*Subscription, 0)
|
||||
@@ -363,9 +389,23 @@ func (m *Manager) GetNotificationStats() []types.SubscriptionStats {
|
||||
return stats
|
||||
}
|
||||
|
||||
// Dispatchers returns all dispatchers as DispatcherConfig sorted by ID
|
||||
// Dispatchers returns all dispatchers as DispatcherConfig sorted by ID.
|
||||
// Includes the cloud dispatcher (ID 0) when configured.
|
||||
func (m *Manager) Dispatchers() []DispatcherConfig {
|
||||
result := make([]DispatcherConfig, 0)
|
||||
|
||||
// Include cloud dispatcher if configured
|
||||
if p := m.cloudDispatcher.Load(); p != nil {
|
||||
if cd, ok := (*p).(*dispatcher.CloudDispatcher); ok {
|
||||
result = append(result, DispatcherConfig{
|
||||
ID: 0,
|
||||
Name: cd.Name,
|
||||
Type: "cloud",
|
||||
Prefix: cd.Prefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.dispatchers.Range(func(id int, d dispatcher.Dispatcher) bool {
|
||||
switch v := d.(type) {
|
||||
case *dispatcher.WebhookDispatcher:
|
||||
@@ -377,15 +417,6 @@ func (m *Manager) Dispatchers() []DispatcherConfig {
|
||||
Template: v.TemplateText,
|
||||
Headers: v.Headers,
|
||||
})
|
||||
case *dispatcher.CloudDispatcher:
|
||||
result = append(result, DispatcherConfig{
|
||||
ID: id,
|
||||
Name: v.Name,
|
||||
Type: "cloud",
|
||||
APIKey: v.APIKey,
|
||||
Prefix: v.Prefix,
|
||||
ExpiresAt: v.ExpiresAt,
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@ func (m *Manager) processLogEvent(logEvent *container.LogEvent) {
|
||||
}
|
||||
|
||||
// Send to the subscription's dispatcher
|
||||
if d, ok := m.dispatchers.Load(sub.DispatcherID); ok {
|
||||
if d, ok := m.getDispatcher(sub.DispatcherID); ok {
|
||||
go m.sendNotification(d, notification, sub.DispatcherID)
|
||||
}
|
||||
return true
|
||||
@@ -177,7 +177,7 @@ func (m *Manager) processStatEvent(event *ContainerStatEvent) {
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if d, ok := m.dispatchers.Load(sub.DispatcherID); ok {
|
||||
if d, ok := m.getDispatcher(sub.DispatcherID); ok {
|
||||
go m.sendNotification(d, notification, sub.DispatcherID)
|
||||
}
|
||||
return true
|
||||
@@ -264,7 +264,7 @@ func (m *Manager) processDockerEvent(event *ContainerEventEntry) {
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if d, ok := m.dispatchers.Load(sub.DispatcherID); ok {
|
||||
if d, ok := m.getDispatcher(sub.DispatcherID); ok {
|
||||
go m.sendNotification(d, notification, sub.DispatcherID)
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -173,15 +173,13 @@ func (s *Subscription) CompileExpressions() error {
|
||||
|
||||
// DispatcherConfig represents a dispatcher configuration
|
||||
type DispatcherConfig struct {
|
||||
ID int `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"` // "webhook", "cloud"
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Template string `json:"template,omitempty" yaml:"template,omitempty"` // Go template for custom payload format
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // Custom HTTP headers
|
||||
APIKey string `json:"apiKey,omitempty" yaml:"apiKey,omitempty"` // API key for cloud dispatcher
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` // API key prefix for cloud dispatcher
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty" yaml:"expiresAt,omitempty"`
|
||||
ID int `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"` // "webhook" or "cloud"
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Template string `json:"template,omitempty" yaml:"template,omitempty"` // Go template for custom payload format
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // Custom HTTP headers
|
||||
Prefix string `json:"prefix,omitempty" yaml:"-"` // Cloud dispatcher API key prefix (not persisted)
|
||||
}
|
||||
|
||||
// Config represents the persisted notification configuration
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
docker_support "github.com/amir20/dozzle/internal/support/docker"
|
||||
"github.com/amir20/dozzle/types"
|
||||
@@ -58,6 +59,44 @@ func (h *persistingNotificationHandler) HandleNotificationConfig(subscriptions [
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *persistingNotificationHandler) SetCloudDispatcher(d dispatcher.Dispatcher) {
|
||||
h.manager.SetCloudDispatcher(d)
|
||||
|
||||
// Persist cloud config to disk so it survives agent restarts
|
||||
cd, ok := d.(*dispatcher.CloudDispatcher)
|
||||
if !ok {
|
||||
log.Warn().Str("type", fmt.Sprintf("%T", d)).Msg("Cloud dispatcher type assertion failed, cannot persist")
|
||||
return
|
||||
}
|
||||
cc := notification.CloudConfig{
|
||||
APIKey: cd.APIKey,
|
||||
Prefix: cd.Prefix,
|
||||
ExpiresAt: cd.ExpiresAt,
|
||||
}
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
log.Error().Err(err).Msg("Could not create data directory for cloud config")
|
||||
return
|
||||
}
|
||||
file, err := os.Create("./data/cloud.yml")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Could not create cloud.yml on agent")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if err := notification.WriteCloudConfig(file, cc); err != nil {
|
||||
log.Error().Err(err).Msg("Could not write cloud.yml on agent")
|
||||
} else {
|
||||
log.Debug().Msg("Persisted cloud.yml on agent")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *persistingNotificationHandler) ClearCloudDispatcher() {
|
||||
h.manager.ClearCloudDispatcher()
|
||||
if err := os.Remove("./data/cloud.yml"); err != nil && !os.IsNotExist(err) {
|
||||
log.Error().Err(err).Msg("Could not remove cloud.yml on agent")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AgentCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
if args.Mode != "server" {
|
||||
return fmt.Errorf("agent command is only available in server mode")
|
||||
@@ -113,6 +152,23 @@ func (a *AgentCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// Load cloud config if available
|
||||
if file, err := os.Open("./data/cloud.yml"); err == nil {
|
||||
cc, err := notification.LoadCloudConfig(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to load cloud config on agent")
|
||||
} else {
|
||||
d, err := dispatcher.NewCloudDispatcher("Dozzle Cloud", cc.APIKey, cc.Prefix, cc.ExpiresAt)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create cloud dispatcher on agent")
|
||||
} else {
|
||||
notificationManager.SetCloudDispatcher(d)
|
||||
log.Info().Msg("Loaded cloud config from disk")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create handler that wraps manager and persists config to disk
|
||||
notificationHandler := &persistingNotificationHandler{
|
||||
manager: notificationManager,
|
||||
|
||||
@@ -92,6 +92,10 @@ func (a *agentService) UpdateNotificationConfig(ctx context.Context, subscriptio
|
||||
return a.client.UpdateNotificationConfig(ctx, subscriptions, dispatchers)
|
||||
}
|
||||
|
||||
func (a *agentService) UpdateCloudConfig(ctx context.Context, cloudConfig *types.CloudConfig) error {
|
||||
return a.client.UpdateCloudConfig(ctx, cloudConfig)
|
||||
}
|
||||
|
||||
func (a *agentService) GetNotificationStats(ctx context.Context) ([]types.SubscriptionStats, error) {
|
||||
return a.client.GetNotificationStats(ctx)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/migration"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
@@ -39,6 +40,8 @@ type MultiHostService struct {
|
||||
manager ClientManager
|
||||
timeout time.Duration
|
||||
notificationManager *notification.Manager
|
||||
cloudConfig *notification.CloudConfig
|
||||
cloudMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHostService {
|
||||
@@ -184,6 +187,7 @@ func (m *MultiHostService) TotalClients() int {
|
||||
}
|
||||
|
||||
const notificationConfigPath = "./data/notifications.yml"
|
||||
const cloudConfigPath = "./data/cloud.yml"
|
||||
|
||||
// StartNotificationManager initializes and starts the notification manager
|
||||
func (m *MultiHostService) StartNotificationManager(ctx context.Context) error {
|
||||
@@ -193,12 +197,15 @@ func (m *MultiHostService) StartNotificationManager(ctx context.Context) error {
|
||||
eventListener := notification.NewContainerEventListener(ctx, clients)
|
||||
m.notificationManager = notification.NewManager(listener, statsListener, eventListener)
|
||||
|
||||
// Migrate old config format before loading (splits cloud into cloud.yml)
|
||||
migration.MigrateCloudConfig(notificationConfigPath, cloudConfigPath)
|
||||
|
||||
// Start first so matcher is available for LoadConfig
|
||||
if err := m.notificationManager.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load config if exists
|
||||
// Load notification config
|
||||
if file, err := os.Open(notificationConfigPath); err == nil {
|
||||
defer file.Close()
|
||||
if err := m.notificationManager.LoadConfig(file); err != nil {
|
||||
@@ -208,6 +215,41 @@ func (m *MultiHostService) StartNotificationManager(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Load cloud config
|
||||
if file, err := os.Open(cloudConfigPath); err == nil {
|
||||
defer file.Close()
|
||||
cc, err := notification.LoadCloudConfig(file)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Could not load cloud config")
|
||||
} else {
|
||||
m.cloudConfig = &cc
|
||||
m.setCloudDispatcherFromConfig(&cc)
|
||||
log.Debug().Str("path", cloudConfigPath).Msg("Loaded cloud config")
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast loaded config to any already-connected agents
|
||||
m.broadcastNotificationConfig()
|
||||
m.broadcastCloudConfig()
|
||||
|
||||
// Re-broadcast when new agents connect so they receive the current config
|
||||
hostCh := make(chan container.Host, 1)
|
||||
m.manager.Subscribe(ctx, hostCh)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case host := <-hostCh:
|
||||
if host.Available {
|
||||
log.Debug().Str("host", host.Name).Msg("New host available, broadcasting config")
|
||||
m.broadcastNotificationConfig()
|
||||
m.broadcastCloudConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -232,9 +274,79 @@ func (m *MultiHostService) saveNotificationConfig() {
|
||||
m.broadcastNotificationConfig()
|
||||
}
|
||||
|
||||
// saveCloudConfig writes the current cloud config to cloudConfigPath and
|
||||
// broadcasts the notification config to all agents so they receive the API key.
|
||||
func (m *MultiHostService) saveCloudConfig() {
|
||||
m.cloudMu.RLock()
|
||||
cc := m.cloudConfig
|
||||
m.cloudMu.RUnlock()
|
||||
|
||||
if cc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
log.Error().Err(err).Msg("Could not create data directory")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Create(cloudConfigPath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Could not create cloud config file")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := notification.WriteCloudConfig(file, *cc); err != nil {
|
||||
log.Error().Err(err).Msg("Could not write cloud config")
|
||||
}
|
||||
|
||||
m.broadcastCloudConfig()
|
||||
}
|
||||
|
||||
// CloudConfig returns the current cloud config, or nil if not set.
|
||||
func (m *MultiHostService) CloudConfig() *notification.CloudConfig {
|
||||
m.cloudMu.RLock()
|
||||
defer m.cloudMu.RUnlock()
|
||||
return m.cloudConfig
|
||||
}
|
||||
|
||||
// SetCloudConfig sets the cloud config, creates the cloud dispatcher, and persists to disk.
|
||||
func (m *MultiHostService) SetCloudConfig(cc *notification.CloudConfig) {
|
||||
m.cloudMu.Lock()
|
||||
m.cloudConfig = cc
|
||||
m.cloudMu.Unlock()
|
||||
m.setCloudDispatcherFromConfig(cc)
|
||||
m.saveCloudConfig()
|
||||
}
|
||||
|
||||
// RemoveCloudConfig clears the cloud config, removes the cloud dispatcher, deletes the file,
|
||||
// and broadcasts the change to all agents so they stop sending to cloud.
|
||||
func (m *MultiHostService) RemoveCloudConfig() {
|
||||
m.cloudMu.Lock()
|
||||
m.cloudConfig = nil
|
||||
m.cloudMu.Unlock()
|
||||
m.notificationManager.ClearCloudDispatcher()
|
||||
if err := os.Remove(cloudConfigPath); err != nil && !os.IsNotExist(err) {
|
||||
log.Error().Err(err).Msg("Could not remove cloud config file")
|
||||
}
|
||||
m.broadcastCloudConfig()
|
||||
}
|
||||
|
||||
// setCloudDispatcherFromConfig creates a CloudDispatcher from the given config and sets it on the manager.
|
||||
func (m *MultiHostService) setCloudDispatcherFromConfig(cc *notification.CloudConfig) {
|
||||
d, err := dispatcher.NewCloudDispatcher("Dozzle Cloud", cc.APIKey, cc.Prefix, cc.ExpiresAt)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Could not create cloud dispatcher from config")
|
||||
return
|
||||
}
|
||||
m.notificationManager.SetCloudDispatcher(d)
|
||||
}
|
||||
|
||||
// NotificationConfigUpdater is an interface for clients that support notification config updates
|
||||
type NotificationConfigUpdater interface {
|
||||
UpdateNotificationConfig(ctx context.Context, subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error
|
||||
UpdateCloudConfig(ctx context.Context, cloudConfig *types.CloudConfig) error
|
||||
}
|
||||
|
||||
// broadcastNotificationConfig sends current notification config to all agent clients
|
||||
@@ -242,7 +354,6 @@ func (m *MultiHostService) broadcastNotificationConfig() {
|
||||
notifSubs := m.notificationManager.Subscriptions()
|
||||
notifDispatchers := m.notificationManager.Dispatchers()
|
||||
|
||||
// Convert notification.Subscription to types.SubscriptionConfig
|
||||
subscriptions := make([]types.SubscriptionConfig, len(notifSubs))
|
||||
for i, sub := range notifSubs {
|
||||
subscriptions[i] = types.SubscriptionConfig{
|
||||
@@ -259,33 +370,30 @@ func (m *MultiHostService) broadcastNotificationConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert notification.DispatcherConfig to types.DispatcherConfig
|
||||
dispatchers := make([]types.DispatcherConfig, len(notifDispatchers))
|
||||
for i, d := range notifDispatchers {
|
||||
dispatchers[i] = types.DispatcherConfig{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
Headers: d.Headers,
|
||||
APIKey: d.APIKey,
|
||||
Prefix: d.Prefix,
|
||||
ExpiresAt: d.ExpiresAt,
|
||||
// Cloud dispatchers are excluded; cloud config is broadcast separately.
|
||||
dispatchers := make([]types.DispatcherConfig, 0, len(notifDispatchers))
|
||||
for _, d := range notifDispatchers {
|
||||
if d.Type == "cloud" {
|
||||
continue
|
||||
}
|
||||
dispatchers = append(dispatchers, types.DispatcherConfig{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
Headers: d.Headers,
|
||||
})
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, client := range m.manager.List() {
|
||||
// Check if client supports notification config updates (agents do, local docker clients don't)
|
||||
if updater, ok := client.(NotificationConfigUpdater); ok {
|
||||
wg.Go(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
if err := updater.UpdateNotificationConfig(ctx, subscriptions, dispatchers); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to broadcast notification config to agent")
|
||||
} else {
|
||||
log.Debug().Int("subscriptions", len(subscriptions)).Int("dispatchers", len(dispatchers)).Msg("Broadcasted notification config to agent")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -293,6 +401,39 @@ func (m *MultiHostService) broadcastNotificationConfig() {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// broadcastCloudConfig sends current cloud config to all agent clients
|
||||
func (m *MultiHostService) broadcastCloudConfig() {
|
||||
m.cloudMu.RLock()
|
||||
ncc := m.cloudConfig
|
||||
m.cloudMu.RUnlock()
|
||||
|
||||
var cc *types.CloudConfig
|
||||
if ncc != nil {
|
||||
cc = &types.CloudConfig{
|
||||
APIKey: ncc.APIKey,
|
||||
Prefix: ncc.Prefix,
|
||||
ExpiresAt: ncc.ExpiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
var count int
|
||||
var wg sync.WaitGroup
|
||||
for _, client := range m.manager.List() {
|
||||
if updater, ok := client.(NotificationConfigUpdater); ok {
|
||||
count++
|
||||
wg.Go(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
if err := updater.UpdateCloudConfig(ctx, cc); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to broadcast cloud config to agent")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
log.Debug().Int("agents", count).Bool("hasCloud", cc != nil).Msg("Broadcasted cloud config")
|
||||
}
|
||||
|
||||
// NotificationHandler returns the notification manager as an agent.NotificationConfigHandler.
|
||||
// This is used in swarm mode to pass the handler to the local agent server.
|
||||
func (m *MultiHostService) NotificationHandler() *notification.Manager {
|
||||
|
||||
@@ -178,3 +178,15 @@ func (m *K8sClusterService) Dispatchers() []notification.DispatcherConfig {
|
||||
func (m *K8sClusterService) FetchAgentNotificationStats() map[int]types.SubscriptionStats {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) CloudConfig() *notification.CloudConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SetCloudConfig(cc *notification.CloudConfig) {
|
||||
// Not supported in k8s mode
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) RemoveCloudConfig() {
|
||||
// Not supported in k8s mode
|
||||
}
|
||||
|
||||
+51
-21
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -20,6 +21,7 @@ type exchangeTokenResponse struct {
|
||||
|
||||
func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
from := r.URL.Query().Get("from")
|
||||
if token == "" {
|
||||
http.Error(w, "missing token parameter", http.StatusBadRequest)
|
||||
return
|
||||
@@ -82,16 +84,13 @@ func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
name := "Dozzle Cloud"
|
||||
|
||||
cloudDispatcher, err := dispatcher.NewCloudDispatcher(name, tokenResp.Key, tokenResp.Prefix, expiresAt)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create cloud dispatcher")
|
||||
http.Error(w, "failed to create cloud dispatcher", http.StatusInternalServerError)
|
||||
return
|
||||
// Save cloud config (also creates the cloud dispatcher and broadcasts to agents)
|
||||
cc := ¬ification.CloudConfig{
|
||||
APIKey: tokenResp.Key,
|
||||
Prefix: tokenResp.Prefix,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
|
||||
id := h.hostService.AddDispatcher(cloudDispatcher)
|
||||
h.hostService.SetCloudConfig(cc)
|
||||
|
||||
if h.config.OnCloudSetup != nil {
|
||||
h.config.OnCloudSetup()
|
||||
@@ -101,24 +100,23 @@ func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) {
|
||||
if base == "/" {
|
||||
base = ""
|
||||
}
|
||||
redirectURL := fmt.Sprintf("%s/notifications#cloudLinkSuccess=%d", base, id)
|
||||
|
||||
var redirectURL string
|
||||
if from == "notifications" {
|
||||
redirectURL = fmt.Sprintf("%s/notifications#cloudLinked", base)
|
||||
} else {
|
||||
redirectURL = fmt.Sprintf("%s/#cloudLinked", base)
|
||||
}
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *handler) cloudStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// Find the cloud dispatcher to get the API key
|
||||
var apiKey string
|
||||
for _, d := range h.hostService.Dispatchers() {
|
||||
if d.Type == "cloud" && d.APIKey != "" {
|
||||
apiKey = d.APIKey
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if apiKey == "" {
|
||||
writeError(w, http.StatusNotFound, "no cloud dispatcher configured")
|
||||
cc := h.hostService.CloudConfig()
|
||||
if cc == nil || cc.APIKey == "" {
|
||||
writeError(w, http.StatusNotFound, "no cloud configuration")
|
||||
return
|
||||
}
|
||||
apiKey := cc.APIKey
|
||||
|
||||
cloudURL := os.Getenv("DOLIGENCE_URL")
|
||||
if cloudURL == "" {
|
||||
@@ -160,3 +158,35 @@ func (h *handler) cloudStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
type cloudConfigResponse struct {
|
||||
Prefix string `json:"prefix"`
|
||||
ExpiresAt *string `json:"expiresAt,omitempty"`
|
||||
Linked bool `json:"linked"`
|
||||
}
|
||||
|
||||
func (h *handler) cloudConfig(w http.ResponseWriter, r *http.Request) {
|
||||
cc := h.hostService.CloudConfig()
|
||||
if cc == nil {
|
||||
writeError(w, http.StatusNotFound, "no cloud configuration")
|
||||
return
|
||||
}
|
||||
|
||||
resp := cloudConfigResponse{
|
||||
Prefix: cc.Prefix,
|
||||
Linked: true,
|
||||
}
|
||||
if cc.ExpiresAt != nil {
|
||||
s := cc.ExpiresAt.Format(time.RFC3339)
|
||||
resp.ExpiresAt = &s
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (h *handler) deleteCloudConfig(w http.ResponseWriter, r *http.Request) {
|
||||
h.hostService.RemoveCloudConfig()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -38,14 +38,13 @@ type NotificationRuleResponse struct {
|
||||
}
|
||||
|
||||
type DispatcherResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Prefix *string `json:"prefix,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Prefix *string `json:"prefix,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationRuleInput struct {
|
||||
@@ -186,14 +185,13 @@ func dispatcherConfigToResponse(d *notification.DispatcherConfig) *DispatcherRes
|
||||
prefix = &d.Prefix
|
||||
}
|
||||
return &DispatcherResponse{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: url,
|
||||
Template: template,
|
||||
Headers: headers,
|
||||
Prefix: prefix,
|
||||
ExpiresAt: d.ExpiresAt,
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: url,
|
||||
Template: template,
|
||||
Headers: headers,
|
||||
Prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,9 @@ type HostService interface {
|
||||
RemoveDispatcher(id int)
|
||||
Dispatchers() []notification.DispatcherConfig
|
||||
FetchAgentNotificationStats() map[int]types.SubscriptionStats
|
||||
CloudConfig() *notification.CloudConfig
|
||||
SetCloudConfig(cc *notification.CloudConfig)
|
||||
RemoveCloudConfig()
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
@@ -186,6 +189,8 @@ func createRouter(h *handler) *chi.Mux {
|
||||
|
||||
// Cloud API
|
||||
r.Get("/cloud/status", h.cloudStatus)
|
||||
r.Get("/cloud/config", h.cloudConfig)
|
||||
r.Delete("/cloud/config", h.deleteCloudConfig)
|
||||
})
|
||||
|
||||
// Public API routes
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: Luk
|
||||
save: Gem
|
||||
add: Tilføj Destination
|
||||
cloud-exists: Kun én Dozzle Cloud destination kan konfigureres
|
||||
cloud-exists: Dozzle Cloud er allerede tilknyttet
|
||||
link-cloud: Link Konto
|
||||
link-cloud-button: Link Dozzle Cloud
|
||||
cloud-settings-hint: For at konfigurere dine administrerede kanaler, gå til
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: Din API-nøgle er ugyldig eller udløbet. Slet venligst og link din konto igen.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Begivenheder i denne periode
|
||||
empty-state:
|
||||
title: Kom i gang med notifikationer
|
||||
description: Vælg hvordan du vil modtage alarmer, når dine containere kræver opmærksomhed.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Linket
|
||||
message: Din konto er blevet succesfuldt linket til Dozzle Cloud.
|
||||
message: Din instans er blevet tilsluttet. Du kan fjerne tilknytningen i indstillinger eller tjekke dit forbrug når som helst.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Styr dine containere eksternt og brug AI til at undersøge problemer på tværs af din klynge.
|
||||
learn-more: Læs mere
|
||||
link-instance: Tilknyt instans
|
||||
relink-instance: Gentilknyt instans
|
||||
connected: Tilsluttet
|
||||
plan: Plan
|
||||
usage: Begivenheder i denne periode
|
||||
dashboard: Dashboard
|
||||
settings: Indstillinger
|
||||
error: Forbindelsesfejl. Gentilknyt venligst din instans.
|
||||
unlink: Fjern tilknytning
|
||||
unlink-confirm: Er du sikker på, at du vil fjerne tilknytningen til Dozzle Cloud? Dette fjerner alle cloud-notifikationsdestinationer.
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: Schließen
|
||||
save: Speichern
|
||||
add: Ziel hinzufügen
|
||||
cloud-exists: Es kann nur ein Dozzle Cloud Ziel konfiguriert werden
|
||||
cloud-exists: Dozzle Cloud ist bereits verknüpft
|
||||
link-cloud: Konto verknüpfen
|
||||
link-cloud-button: Dozzle Cloud verknüpfen
|
||||
cloud-settings-hint: Um Ihre verwalteten Kanäle zu konfigurieren, gehen Sie zu
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: Ihr API-Schlüssel ist ungültig oder abgelaufen. Bitte löschen und verknüpfen Sie Ihr Konto erneut.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Ereignisse in diesem Zeitraum
|
||||
empty-state:
|
||||
title: Erste Schritte mit Benachrichtigungen
|
||||
description: Wählen Sie aus, wie Sie Alarme erhalten möchten, wenn Ihre Container Aufmerksamkeit benötigen.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud verknüpft
|
||||
message: Ihr Konto wurde erfolgreich mit Dozzle Cloud verknüpft.
|
||||
message: Ihre Instanz wurde erfolgreich verbunden. Sie können die Verknüpfung in den Einstellungen aufheben oder Ihre Nutzung jederzeit einsehen.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Steuern Sie Ihre Container aus der Ferne und nutzen Sie KI, um Probleme in Ihrem Cluster zu untersuchen.
|
||||
learn-more: Mehr erfahren
|
||||
link-instance: Instanz verknüpfen
|
||||
relink-instance: Instanz erneut verknüpfen
|
||||
connected: Verbunden
|
||||
plan: Plan
|
||||
usage: Ereignisse in diesem Zeitraum
|
||||
dashboard: Dashboard
|
||||
settings: Einstellungen
|
||||
error: Verbindungsfehler. Bitte verknüpfen Sie Ihre Instanz erneut.
|
||||
unlink: Verknüpfung aufheben
|
||||
unlink-confirm: Sind Sie sicher, dass Sie die Verknüpfung mit Dozzle Cloud aufheben möchten? Dies entfernt alle Cloud-Benachrichtigungsziele.
|
||||
|
||||
+21
-2
@@ -279,7 +279,7 @@ notifications:
|
||||
close: Close
|
||||
save: Save
|
||||
add: Add Destination
|
||||
cloud-exists: Only one Dozzle Cloud destination can be configured
|
||||
cloud-exists: Dozzle Cloud is already linked
|
||||
link-cloud: Link Account
|
||||
link-cloud-button: Link Dozzle Cloud
|
||||
cloud-settings-hint: To configure your managed channels, go to
|
||||
@@ -288,6 +288,25 @@ notifications:
|
||||
cloud-relink: Your API key is invalid or expired. Please delete and link your account again.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Events this period
|
||||
empty-state:
|
||||
title: Get started with notifications
|
||||
description: Choose how you want to receive alerts when your containers need attention.
|
||||
cloud-subtitle: AI summaries, remote control & more
|
||||
webhook-subtitle: Send alerts to any endpoint
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Linked
|
||||
message: Your account has been successfully linked to Dozzle Cloud.
|
||||
message: Your instance has been connected successfully. You can unlink it in settings or check your usage anytime.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Control your containers remotely and use AI to investigate issues across your cluster.
|
||||
learn-more: Learn more
|
||||
link-instance: Link instance
|
||||
relink-instance: Re-link instance
|
||||
connected: Connected
|
||||
plan: Plan
|
||||
usage: Events this period
|
||||
dashboard: Dashboard
|
||||
settings: Settings
|
||||
error: Connection error. Please re-link your instance.
|
||||
unlink: Unlink
|
||||
unlink-confirm: Are you sure you want to unlink from Dozzle Cloud? This will remove all cloud notification destinations.
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: Cerrar
|
||||
save: Guardar
|
||||
add: Añadir Destino
|
||||
cloud-exists: Solo se puede configurar un destino de Dozzle Cloud
|
||||
cloud-exists: Dozzle Cloud ya está vinculado
|
||||
link-cloud: Vincular Cuenta
|
||||
link-cloud-button: Vincular Dozzle Cloud
|
||||
cloud-settings-hint: Para configurar sus canales administrados, vaya a
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: Su clave API es inválida o ha expirado. Por favor, elimine y vincule su cuenta de nuevo.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Eventos en este período
|
||||
empty-state:
|
||||
title: Comenzar con las notificaciones
|
||||
description: Elija cómo desea recibir alertas cuando sus contenedores necesiten atención.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Vinculado
|
||||
message: Su cuenta se ha vinculado correctamente a Dozzle Cloud.
|
||||
message: Su instancia se ha conectado correctamente. Puede desvincularla en la configuración o consultar su uso en cualquier momento.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Controle sus contenedores de forma remota y use IA para investigar problemas en su clúster.
|
||||
learn-more: Más información
|
||||
link-instance: Vincular instancia
|
||||
relink-instance: Revincular instancia
|
||||
connected: Conectado
|
||||
plan: Plan
|
||||
usage: Eventos en este período
|
||||
dashboard: Panel
|
||||
settings: Configuración
|
||||
error: Error de conexión. Por favor, revincule su instancia.
|
||||
unlink: Desvincular
|
||||
unlink-confirm: ¿Está seguro de que desea desvincular de Dozzle Cloud? Esto eliminará todos los destinos de notificación en la nube.
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: Fermer
|
||||
save: Enregistrer
|
||||
add: Ajouter la Destination
|
||||
cloud-exists: Une seule destination Dozzle Cloud peut être configurée
|
||||
cloud-exists: Dozzle Cloud est déjà lié
|
||||
link-cloud: Lier le Compte
|
||||
link-cloud-button: Lier Dozzle Cloud
|
||||
cloud-settings-hint: Pour configurer vos canaux gérés, allez à
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: Votre clé API est invalide ou expirée. Veuillez supprimer et relier votre compte.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Événements pour cette période
|
||||
empty-state:
|
||||
title: Commencer avec les notifications
|
||||
description: Choisissez comment vous souhaitez recevoir des alertes lorsque vos conteneurs nécessitent une attention.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Lié
|
||||
message: Votre compte a été lié avec succès à Dozzle Cloud.
|
||||
message: Votre instance a été connectée avec succès. Vous pouvez la délier dans les paramètres ou consulter votre utilisation à tout moment.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Contrôlez vos conteneurs à distance et utilisez l'IA pour examiner les problèmes dans votre cluster.
|
||||
learn-more: En savoir plus
|
||||
link-instance: Lier l'instance
|
||||
relink-instance: Relier l'instance
|
||||
connected: Connecté
|
||||
plan: Plan
|
||||
usage: Événements pour cette période
|
||||
dashboard: Tableau de bord
|
||||
settings: Paramètres
|
||||
error: Erreur de connexion. Veuillez relier votre instance.
|
||||
unlink: Délier
|
||||
unlink-confirm: Êtes-vous sûr de vouloir délier de Dozzle Cloud ? Cela supprimera toutes les destinations de notification cloud.
|
||||
|
||||
+19
-2
@@ -282,7 +282,7 @@ notifications:
|
||||
close: Tutup
|
||||
save: Simpan
|
||||
add: Tambah Tujuan
|
||||
cloud-exists: Hanya satu tujuan Dozzle Cloud yang dapat dikonfigurasi
|
||||
cloud-exists: Dozzle Cloud sudah tertaut
|
||||
link-cloud: Tautkan Akun
|
||||
link-cloud-button: Tautkan Dozzle Cloud
|
||||
cloud-settings-hint: Untuk mengonfigurasi saluran terkelola Anda, buka
|
||||
@@ -291,6 +291,23 @@ notifications:
|
||||
cloud-relink: Kunci API Anda tidak valid atau kedaluwarsa. Silakan hapus dan hubungkan akun Anda lagi.
|
||||
cloud-plan: Paket
|
||||
cloud-usage: Event periode ini
|
||||
empty-state:
|
||||
title: Mulai dengan notifikasi
|
||||
description: Pilih cara Anda ingin menerima peringatan saat kontainer Anda membutuhkan perhatian.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Tertaut
|
||||
message: Akun Anda telah berhasil ditautkan ke Dozzle Cloud.
|
||||
message: Instans Anda telah berhasil terhubung. Anda dapat melepaskan tautan di pengaturan atau memeriksa penggunaan Anda kapan saja.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Kendalikan kontainer Anda dari jarak jauh dan gunakan AI untuk menyelidiki masalah di seluruh kluster Anda.
|
||||
learn-more: Pelajari lebih lanjut
|
||||
link-instance: Tautkan instans
|
||||
relink-instance: Tautkan ulang instans
|
||||
connected: Terhubung
|
||||
plan: Paket
|
||||
usage: Event periode ini
|
||||
dashboard: Dasbor
|
||||
settings: Pengaturan
|
||||
error: Kesalahan koneksi. Silakan tautkan ulang instans Anda.
|
||||
unlink: Lepaskan tautan
|
||||
unlink-confirm: Apakah Anda yakin ingin melepaskan tautan dari Dozzle Cloud? Ini akan menghapus semua tujuan notifikasi cloud.
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: Chiudi
|
||||
save: Salva
|
||||
add: Aggiungi Destinazione
|
||||
cloud-exists: È possibile configurare solo una destinazione Dozzle Cloud
|
||||
cloud-exists: Dozzle Cloud è già collegato
|
||||
link-cloud: Collega Account
|
||||
link-cloud-button: Collega Dozzle Cloud
|
||||
cloud-settings-hint: Per configurare i tuoi canali gestiti, vai a
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: La tua chiave API non è valida o è scaduta. Elimina e collega nuovamente il tuo account.
|
||||
cloud-plan: Piano
|
||||
cloud-usage: Eventi in questo periodo
|
||||
empty-state:
|
||||
title: Inizia con le notifiche
|
||||
description: Scegli come vuoi ricevere avvisi quando i tuoi container richiedono attenzione.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Collegato
|
||||
message: Il tuo account è stato collegato con successo a Dozzle Cloud.
|
||||
message: La tua istanza è stata connessa con successo. Puoi scollegarla nelle impostazioni o controllare il tuo utilizzo in qualsiasi momento.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Controlla i tuoi container da remoto e usa l'AI per investigare problemi nel tuo cluster.
|
||||
learn-more: Scopri di più
|
||||
link-instance: Collega istanza
|
||||
relink-instance: Ricollega istanza
|
||||
connected: Connesso
|
||||
plan: Piano
|
||||
usage: Eventi in questo periodo
|
||||
dashboard: Dashboard
|
||||
settings: Impostazioni
|
||||
error: Errore di connessione. Ricollega la tua istanza.
|
||||
unlink: Scollega
|
||||
unlink-confirm: Sei sicuro di voler scollegare da Dozzle Cloud? Questo rimuoverà tutte le destinazioni di notifica cloud.
|
||||
|
||||
+19
-2
@@ -273,7 +273,7 @@ notifications:
|
||||
close: 닫기
|
||||
save: 저장
|
||||
add: 목적지 추가
|
||||
cloud-exists: Dozzle Cloud 목적지는 하나만 설정할 수 있습니다
|
||||
cloud-exists: Dozzle Cloud가 이미 연결되어 있습니다
|
||||
link-cloud: 계정 연결
|
||||
link-cloud-button: Dozzle Cloud 연결
|
||||
cloud-settings-hint: 관리 채널을 설정하려면 다음으로 이동하세요
|
||||
@@ -282,6 +282,23 @@ notifications:
|
||||
cloud-relink: API 키가 유효하지 않거나 만료되었습니다. 삭제 후 계정을 다시 연결해 주세요.
|
||||
cloud-plan: 플랜
|
||||
cloud-usage: 이번 기간 이벤트
|
||||
empty-state:
|
||||
title: 알림 시작하기
|
||||
description: 컨테이너에 문제가 생겼을 때 알림을 받을 방법을 선택하세요.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud 연결됨
|
||||
message: 계정이 Dozzle Cloud에 성공적으로 연결되었습니다.
|
||||
message: 인스턴스가 성공적으로 연결되었습니다. 설정에서 연결을 해제하거나 사용량을 언제든지 확인할 수 있습니다.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: 원격으로 컨테이너를 제어하고 AI를 사용하여 클러스터 전반의 문제를 조사하세요.
|
||||
learn-more: 자세히 알아보기
|
||||
link-instance: 인스턴스 연결
|
||||
relink-instance: 인스턴스 재연결
|
||||
connected: 연결됨
|
||||
plan: 플랜
|
||||
usage: 이번 기간 이벤트
|
||||
dashboard: 대시보드
|
||||
settings: 설정
|
||||
error: 연결 오류. 인스턴스를 다시 연결해 주세요.
|
||||
unlink: 연결 해제
|
||||
unlink-confirm: Dozzle Cloud 연결을 해제하시겠습니까? 모든 클라우드 알림 목적지가 삭제됩니다.
|
||||
|
||||
+19
-2
@@ -271,7 +271,7 @@ notifications:
|
||||
close: Sluiten
|
||||
save: Opslaan
|
||||
add: Bestemming toevoegen
|
||||
cloud-exists: Er kan slechts één Dozzle Cloud-bestemming worden geconfigureerd
|
||||
cloud-exists: Dozzle Cloud is al gekoppeld
|
||||
link-cloud: Account koppelen
|
||||
link-cloud-button: Dozzle Cloud koppelen
|
||||
cloud-settings-hint: Om je beheerde kanalen te configureren, ga naar
|
||||
@@ -280,6 +280,23 @@ notifications:
|
||||
cloud-relink: Uw API-sleutel is ongeldig of verlopen. Verwijder en koppel uw account opnieuw.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Gebeurtenissen deze periode
|
||||
empty-state:
|
||||
title: Aan de slag met meldingen
|
||||
description: Kies hoe je waarschuwingen wilt ontvangen wanneer je containers aandacht nodig hebben.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud gekoppeld
|
||||
message: Je account is succesvol gekoppeld aan Dozzle Cloud.
|
||||
message: Je instantie is succesvol verbonden. Je kunt de koppeling ongedaan maken in de instellingen of je gebruik op elk moment bekijken.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Beheer je containers op afstand en gebruik AI om problemen in je cluster te onderzoeken.
|
||||
learn-more: Meer informatie
|
||||
link-instance: Instantie koppelen
|
||||
relink-instance: Instantie opnieuw koppelen
|
||||
connected: Verbonden
|
||||
plan: Plan
|
||||
usage: Gebeurtenissen deze periode
|
||||
dashboard: Dashboard
|
||||
settings: Instellingen
|
||||
error: Verbindingsfout. Koppel je instantie opnieuw.
|
||||
unlink: Ontkoppelen
|
||||
unlink-confirm: Weet je zeker dat je de koppeling met Dozzle Cloud wilt opheffen? Dit verwijdert alle cloudmeldingsbestemmingen.
|
||||
|
||||
+19
-2
@@ -277,7 +277,7 @@ notifications:
|
||||
close: Zamknij
|
||||
save: Zapisz
|
||||
add: Dodaj Miejsce Docelowe
|
||||
cloud-exists: Można skonfigurować tylko jedno miejsce docelowe Dozzle Cloud
|
||||
cloud-exists: Dozzle Cloud jest już połączony
|
||||
link-cloud: Połącz Konto
|
||||
link-cloud-button: Połącz Dozzle Cloud
|
||||
cloud-settings-hint: Aby skonfigurować zarządzane kanały, przejdź do
|
||||
@@ -286,6 +286,23 @@ notifications:
|
||||
cloud-relink: Twój klucz API jest nieprawidłowy lub wygasł. Usuń i połącz swoje konto ponownie.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Zdarzenia w tym okresie
|
||||
empty-state:
|
||||
title: Zacznij korzystać z powiadomień
|
||||
description: Wybierz, jak chcesz otrzymywać alerty, gdy Twoje kontenery wymagają uwagi.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Połączony
|
||||
message: Twoje konto zostało pomyślnie połączone z Dozzle Cloud.
|
||||
message: Twoja instancja została pomyślnie połączona. Możesz rozłączyć ją w ustawieniach lub sprawdzić swoje użycie w dowolnym momencie.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Zarządzaj kontenerami zdalnie i wykorzystuj AI do badania problemów w całym klastrze.
|
||||
learn-more: Dowiedz się więcej
|
||||
link-instance: Połącz instancję
|
||||
relink-instance: Połącz ponownie instancję
|
||||
connected: Połączony
|
||||
plan: Plan
|
||||
usage: Zdarzenia w tym okresie
|
||||
dashboard: Panel
|
||||
settings: Ustawienia
|
||||
error: Błąd połączenia. Połącz ponownie swoją instancję.
|
||||
unlink: Rozłącz
|
||||
unlink-confirm: Czy na pewno chcesz rozłączyć się z Dozzle Cloud? Spowoduje to usunięcie wszystkich miejsc docelowych powiadomień w chmurze.
|
||||
|
||||
+19
-2
@@ -279,7 +279,7 @@ notifications:
|
||||
close: Fechar
|
||||
save: Guardar
|
||||
add: Adicionar Destino
|
||||
cloud-exists: Apenas um destino Dozzle Cloud pode ser configurado
|
||||
cloud-exists: Dozzle Cloud já está ligado
|
||||
link-cloud: Ligar Conta
|
||||
link-cloud-button: Ligar Dozzle Cloud
|
||||
cloud-settings-hint: Para configurar os seus canais geridos, vá a
|
||||
@@ -288,6 +288,23 @@ notifications:
|
||||
cloud-relink: A sua chave API é inválida ou expirou. Por favor, elimine e ligue a sua conta novamente.
|
||||
cloud-plan: Plano
|
||||
cloud-usage: Eventos neste período
|
||||
empty-state:
|
||||
title: Comece com as notificações
|
||||
description: Escolha como pretende receber alertas quando os seus contentores necessitam de atenção.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Ligado
|
||||
message: A sua conta foi ligada com sucesso ao Dozzle Cloud.
|
||||
message: A sua instância foi ligada com sucesso. Pode desligar nas definições ou verificar a sua utilização a qualquer momento.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Controle os seus contentores remotamente e use IA para investigar problemas no seu cluster.
|
||||
learn-more: Saber mais
|
||||
link-instance: Ligar instância
|
||||
relink-instance: Religar instância
|
||||
connected: Ligado
|
||||
plan: Plano
|
||||
usage: Eventos neste período
|
||||
dashboard: Painel
|
||||
settings: Definições
|
||||
error: Erro de ligação. Por favor, religue a sua instância.
|
||||
unlink: Desligar
|
||||
unlink-confirm: Tem a certeza de que pretende desligar do Dozzle Cloud? Isto irá remover todos os destinos de notificação na nuvem.
|
||||
|
||||
+19
-2
@@ -269,7 +269,7 @@ notifications:
|
||||
close: Fechar
|
||||
save: Salvar
|
||||
add: Adicionar Destino
|
||||
cloud-exists: Apenas um destino Dozzle Cloud pode ser configurado
|
||||
cloud-exists: Dozzle Cloud já está vinculado
|
||||
link-cloud: Vincular Conta
|
||||
link-cloud-button: Vincular Dozzle Cloud
|
||||
cloud-settings-hint: Para configurar seus canais gerenciados, vá para
|
||||
@@ -278,6 +278,23 @@ notifications:
|
||||
cloud-relink: Sua chave API é inválida ou expirou. Por favor, exclua e vincule sua conta novamente.
|
||||
cloud-plan: Plano
|
||||
cloud-usage: Eventos neste período
|
||||
empty-state:
|
||||
title: Comece com as notificações
|
||||
description: Escolha como você deseja receber alertas quando seus containers precisarem de atenção.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Vinculado
|
||||
message: Sua conta foi vinculada com sucesso ao Dozzle Cloud.
|
||||
message: Sua instância foi conectada com sucesso. Você pode desvincular nas configurações ou verificar seu uso a qualquer momento.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Controle seus containers remotamente e use IA para investigar problemas em todo o seu cluster.
|
||||
learn-more: Saiba mais
|
||||
link-instance: Vincular instância
|
||||
relink-instance: Revincular instância
|
||||
connected: Conectado
|
||||
plan: Plano
|
||||
usage: Eventos neste período
|
||||
dashboard: Painel
|
||||
settings: Configurações
|
||||
error: Erro de conexão. Por favor, revincule sua instância.
|
||||
unlink: Desvincular
|
||||
unlink-confirm: Tem certeza de que deseja desvincular do Dozzle Cloud? Isso removerá todos os destinos de notificação na nuvem.
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: Закрыть
|
||||
save: Сохранить
|
||||
add: Добавить Назначение
|
||||
cloud-exists: Можно настроить только одно назначение Dozzle Cloud
|
||||
cloud-exists: Dozzle Cloud уже привязан
|
||||
link-cloud: Связать Аккаунт
|
||||
link-cloud-button: Связать Dozzle Cloud
|
||||
cloud-settings-hint: Чтобы настроить управляемые каналы, перейдите в
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: Ваш API-ключ недействителен или истёк. Пожалуйста, удалите и привяжите аккаунт заново.
|
||||
cloud-plan: План
|
||||
cloud-usage: События за этот период
|
||||
empty-state:
|
||||
title: Начните с уведомлений
|
||||
description: Выберите, как вы хотите получать оповещения, когда ваши контейнеры требуют внимания.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Связан
|
||||
message: Ваш аккаунт успешно связан с Dozzle Cloud.
|
||||
message: Ваш экземпляр успешно подключён. Вы можете отвязать его в настройках или проверить использование в любое время.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Управляйте контейнерами удалённо и используйте ИИ для расследования проблем в вашем кластере.
|
||||
learn-more: Узнать больше
|
||||
link-instance: Привязать экземпляр
|
||||
relink-instance: Привязать повторно
|
||||
connected: Подключён
|
||||
plan: План
|
||||
usage: События за этот период
|
||||
dashboard: Панель управления
|
||||
settings: Настройки
|
||||
error: Ошибка подключения. Пожалуйста, привяжите экземпляр повторно.
|
||||
unlink: Отвязать
|
||||
unlink-confirm: Вы уверены, что хотите отвязать от Dozzle Cloud? Это удалит все облачные назначения уведомлений.
|
||||
|
||||
+19
-2
@@ -275,7 +275,7 @@ notifications:
|
||||
close: Zapri
|
||||
save: Shrani
|
||||
add: Dodaj Cilj
|
||||
cloud-exists: Nastavljen je lahko samo en cilj Dozzle Cloud
|
||||
cloud-exists: Dozzle Cloud je že povezan
|
||||
link-cloud: Poveži Račun
|
||||
link-cloud-button: Poveži Dozzle Cloud
|
||||
cloud-settings-hint: Za nastavitev upravljanih kanalov pojdite na
|
||||
@@ -284,6 +284,23 @@ notifications:
|
||||
cloud-relink: Vaš API ključ je neveljaven ali potekel. Prosimo, izbrišite in ponovno povežite svoj račun.
|
||||
cloud-plan: Načrt
|
||||
cloud-usage: Dogodki v tem obdobju
|
||||
empty-state:
|
||||
title: Začnite z obvestili
|
||||
description: Izberite, kako želite prejemati opozorila, ko vaši zabojniki potrebujejo pozornost.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Povezan
|
||||
message: Vaš račun je bil uspešno povezan z Dozzle Cloud.
|
||||
message: Vaša instanca je bila uspešno povezana. Povezavo lahko prekinete v nastavitvah ali kadar koli preverite svojo uporabo.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Upravljajte svoje zabojnike na daljavo in uporabite UI za preiskovanje težav v vašem grozdu.
|
||||
learn-more: Več o tem
|
||||
link-instance: Poveži instanco
|
||||
relink-instance: Ponovno poveži instanco
|
||||
connected: Povezano
|
||||
plan: Načrt
|
||||
usage: Dogodki v tem obdobju
|
||||
dashboard: Nadzorna plošča
|
||||
settings: Nastavitve
|
||||
error: Napaka povezave. Prosimo, ponovno povežite svojo instanco.
|
||||
unlink: Prekini povezavo
|
||||
unlink-confirm: Ali ste prepričani, da želite prekiniti povezavo z Dozzle Cloud? To bo odstranilo vse cilje obvestil v oblaku.
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: Kapat
|
||||
save: Kaydet
|
||||
add: Hedef Ekle
|
||||
cloud-exists: Yalnızca bir Dozzle Cloud hedefi yapılandırılabilir
|
||||
cloud-exists: Dozzle Cloud zaten bağlı
|
||||
link-cloud: Hesabı Bağla
|
||||
link-cloud-button: Dozzle Cloud'u Bağla
|
||||
cloud-settings-hint: Yönetilen kanallarınızı yapılandırmak için şuraya gidin
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: API anahtarınız geçersiz veya süresi dolmuş. Lütfen silin ve hesabınızı tekrar bağlayın.
|
||||
cloud-plan: Plan
|
||||
cloud-usage: Bu dönemdeki olaylar
|
||||
empty-state:
|
||||
title: Bildirimlerle başlayın
|
||||
description: Konteynerleriniz dikkat gerektirdiğinde nasıl uyarı almak istediğinizi seçin.
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Bağlandı
|
||||
message: Hesabınız Dozzle Cloud'a başarıyla bağlandı.
|
||||
message: Örneğiniz başarıyla bağlandı. Ayarlardan bağlantıyı kaldırabilir veya kullanımınızı istediğiniz zaman kontrol edebilirsiniz.
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: Konteynerlerinizi uzaktan kontrol edin ve kümenizde sorunları araştırmak için yapay zeka kullanın.
|
||||
learn-more: Daha fazla bilgi
|
||||
link-instance: Örneği bağla
|
||||
relink-instance: Örneği yeniden bağla
|
||||
connected: Bağlı
|
||||
plan: Plan
|
||||
usage: Bu dönemdeki olaylar
|
||||
dashboard: Pano
|
||||
settings: Ayarlar
|
||||
error: Bağlantı hatası. Lütfen örneğinizi yeniden bağlayın.
|
||||
unlink: Bağlantıyı kaldır
|
||||
unlink-confirm: Dozzle Cloud bağlantısını kaldırmak istediğinizden emin misiniz? Bu, tüm bulut bildirim hedeflerini kaldıracaktır.
|
||||
|
||||
+19
-2
@@ -273,7 +273,7 @@ notifications:
|
||||
close: 關閉
|
||||
save: 儲存
|
||||
add: 新增目標
|
||||
cloud-exists: 只能設定一個 Dozzle Cloud 目標
|
||||
cloud-exists: Dozzle Cloud 已連結
|
||||
link-cloud: 連結帳戶
|
||||
link-cloud-button: 連結 Dozzle Cloud
|
||||
cloud-settings-hint: 若要設定您的管理頻道,請前往
|
||||
@@ -282,6 +282,23 @@ notifications:
|
||||
cloud-relink: 您的 API 金鑰無效或已過期。請刪除並重新關聯您的帳戶。
|
||||
cloud-plan: 方案
|
||||
cloud-usage: 本期事件數
|
||||
empty-state:
|
||||
title: 開始使用通知
|
||||
description: 選擇當您的容器需要關注時,您希望如何接收警報。
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud 已連結
|
||||
message: 您的帳戶已成功連結至 Dozzle Cloud。
|
||||
message: 您的實例已成功連線。您可以在設定中取消連結,或隨時查看使用量。
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: 遠端控制您的容器,並使用 AI 調查叢集中的問題。
|
||||
learn-more: 瞭解更多
|
||||
link-instance: 連結實例
|
||||
relink-instance: 重新連結實例
|
||||
connected: 已連線
|
||||
plan: 方案
|
||||
usage: 本期事件數
|
||||
dashboard: 儀表板
|
||||
settings: 設定
|
||||
error: 連線錯誤。請重新連結您的實例。
|
||||
unlink: 取消連結
|
||||
unlink-confirm: 確定要取消與 Dozzle Cloud 的連結嗎?這將移除所有雲端通知目標。
|
||||
|
||||
+19
-2
@@ -270,7 +270,7 @@ notifications:
|
||||
close: 关闭
|
||||
save: 保存
|
||||
add: 添加目标
|
||||
cloud-exists: 只能配置一个 Dozzle Cloud 目标
|
||||
cloud-exists: Dozzle Cloud 已关联
|
||||
link-cloud: 关联账户
|
||||
link-cloud-button: 关联 Dozzle Cloud
|
||||
cloud-settings-hint: 要配置您的托管频道,请前往
|
||||
@@ -279,6 +279,23 @@ notifications:
|
||||
cloud-relink: 您的 API 密钥无效或已过期。请删除并重新关联您的账户。
|
||||
cloud-plan: 计划
|
||||
cloud-usage: 本期事件数
|
||||
empty-state:
|
||||
title: 开始使用通知
|
||||
description: 选择当您的容器需要关注时,您希望如何接收警报。
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud 已关联
|
||||
message: 您的账户已成功关联到 Dozzle Cloud。
|
||||
message: 您的实例已成功连接。您可以在设置中取消关联,或随时查看使用情况。
|
||||
cloud:
|
||||
title: Dozzle Cloud
|
||||
description: 远程控制您的容器,并使用 AI 调查集群中的问题。
|
||||
learn-more: 了解更多
|
||||
link-instance: 关联实例
|
||||
relink-instance: 重新关联实例
|
||||
connected: 已连接
|
||||
plan: 计划
|
||||
usage: 本期事件数
|
||||
dashboard: 仪表盘
|
||||
settings: 设置
|
||||
error: 连接错误。请重新关联您的实例。
|
||||
unlink: 取消关联
|
||||
unlink-confirm: 确定要取消与 Dozzle Cloud 的关联吗?这将移除所有云通知目标。
|
||||
|
||||
@@ -126,10 +126,8 @@ func main() {
|
||||
|
||||
// Create cloud tool client — does nothing until Notify() is called
|
||||
apiKeyFunc := func() string {
|
||||
for _, d := range hostService.Dispatchers() {
|
||||
if d.Type == "cloud" && d.APIKey != "" {
|
||||
return d.APIKey
|
||||
}
|
||||
if cc := hostService.CloudConfig(); cc != nil {
|
||||
return cc.APIKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ service AgentService {
|
||||
rpc ContainerExec(stream ContainerExecRequest) returns (stream ContainerExecResponse) {}
|
||||
rpc ContainerAttach(stream ContainerAttachRequest) returns (stream ContainerAttachResponse) {}
|
||||
rpc UpdateNotificationConfig(UpdateNotificationConfigRequest) returns (UpdateNotificationConfigResponse) {}
|
||||
rpc UpdateCloudConfig(UpdateCloudConfigRequest) returns (UpdateCloudConfigResponse) {}
|
||||
rpc GetNotificationStats(GetNotificationStatsRequest) returns (GetNotificationStatsResponse) {}
|
||||
}
|
||||
|
||||
@@ -150,6 +151,12 @@ message UpdateNotificationConfigRequest {
|
||||
|
||||
message UpdateNotificationConfigResponse {}
|
||||
|
||||
message UpdateCloudConfigRequest {
|
||||
NotificationCloudConfig cloudConfig = 1;
|
||||
}
|
||||
|
||||
message UpdateCloudConfigResponse {}
|
||||
|
||||
message GetNotificationStatsRequest {}
|
||||
|
||||
message GetNotificationStatsResponse {
|
||||
|
||||
+8
-3
@@ -119,9 +119,14 @@ message NotificationDispatcher {
|
||||
string url = 4;
|
||||
string template = 5;
|
||||
map<string, string> headers = 6;
|
||||
string apiKey = 7;
|
||||
string prefix = 8;
|
||||
google.protobuf.Timestamp expiresAt = 9;
|
||||
// Fields 7-9 removed (cloud fields moved to NotificationCloudConfig)
|
||||
reserved 7, 8, 9;
|
||||
}
|
||||
|
||||
message NotificationCloudConfig {
|
||||
string apiKey = 1;
|
||||
string prefix = 2;
|
||||
google.protobuf.Timestamp expiresAt = 3;
|
||||
}
|
||||
|
||||
message NotificationSubscriptionStats {
|
||||
|
||||
+12
-8
@@ -83,15 +83,19 @@ type SubscriptionStats struct {
|
||||
TriggeredContainerIDs []string `json:"triggeredContainerIds"`
|
||||
}
|
||||
|
||||
// DispatcherConfig represents a notification dispatcher configuration
|
||||
type DispatcherConfig struct {
|
||||
ID int
|
||||
Name string
|
||||
Type string
|
||||
URL string
|
||||
Template string
|
||||
Headers map[string]string
|
||||
// CloudConfig holds the cloud API key and metadata for broadcasting to agents.
|
||||
type CloudConfig struct {
|
||||
APIKey string
|
||||
Prefix string
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
|
||||
// DispatcherConfig represents a dispatcher configuration
|
||||
type DispatcherConfig struct {
|
||||
ID int
|
||||
Name string
|
||||
Type string
|
||||
URL string
|
||||
Template string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user