mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 11:44:45 +00:00
chore: makes the experience better for cloud linking (#4393)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="card bg-base-100">
|
||||
<div class="card bg-base-100 hover:border-primary cursor-pointer border border-transparent" @click="editDestination">
|
||||
<div class="card-body gap-2 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg">
|
||||
@@ -16,7 +16,7 @@
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div class="dropdown dropdown-end" @click.stop>
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm btn-square">
|
||||
<ion:ellipsis-vertical />
|
||||
</label>
|
||||
|
||||
@@ -72,12 +72,21 @@
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<!-- Cloud linked success (when editing cloud with apiKey) -->
|
||||
<fieldset v-if="type === 'cloud' && destination?.apiKey" class="fieldset">
|
||||
<div class="alert alert-success">
|
||||
<mdi:check-circle class="text-xl" />
|
||||
<span>{{ $t("notifications.destination-form.cloud-linked") }}</span>
|
||||
<!-- Cloud linked success (when editing cloud with prefix) -->
|
||||
<fieldset v-if="type === 'cloud' && destination?.prefix" class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.api-key") }}</legend>
|
||||
<div class="join w-full">
|
||||
<input type="text" :value="destination.prefix + '...'" readonly class="input join-item input-success w-full" />
|
||||
<span class="join-item btn btn-success pointer-events-none">
|
||||
<mdi:check class="text-lg" />
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-base-content/60 mt-2 text-sm">
|
||||
{{ $t("notifications.destination-form.cloud-settings-hint") }}
|
||||
<a :href="cloudSettingsUrl" target="_blank" class="link link-primary">
|
||||
{{ $t("notifications.destination-form.cloud-settings-link") }}
|
||||
</a>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<!-- Link Dozzle Cloud (only for cloud type, when creating or not linked) -->
|
||||
@@ -258,10 +267,9 @@ const isSaving = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const testResult = ref<TestWebhookResult | null>(null);
|
||||
|
||||
const cloudLinkUrl = computed(() => {
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
return `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
});
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
const cloudLinkUrl = `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
const cloudSettingsUrl = `${__CLOUD_URL__}/settings`;
|
||||
|
||||
function selectPayloadFormat(format: PayloadFormat) {
|
||||
payloadFormat.value = format;
|
||||
|
||||
@@ -3,7 +3,7 @@ type Toast = {
|
||||
createdAt: Date;
|
||||
title?: string;
|
||||
message: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
type: "error" | "warning" | "info";
|
||||
action?: {
|
||||
label: string;
|
||||
handler: () => void;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
v-for="dest in dispatchers"
|
||||
:key="dest.id"
|
||||
:destination="dest"
|
||||
:on-updated="fetchDispatchers"
|
||||
:on-updated="fetchAll"
|
||||
:existing-dispatchers="dispatchers"
|
||||
class="w-full md:w-72"
|
||||
/>
|
||||
@@ -76,7 +76,9 @@ import DestinationForm from "@/components/Notification/DestinationForm.vue";
|
||||
import DestinationCard from "@/components/Notification/DestinationCard.vue";
|
||||
|
||||
const showDrawer = useDrawer();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { showToast } = useToast();
|
||||
|
||||
// State
|
||||
const alerts = ref<NotificationRule[]>([]);
|
||||
@@ -92,31 +94,28 @@ async function fetchDispatchers() {
|
||||
dispatchers.value = await res.json();
|
||||
}
|
||||
|
||||
fetchAlerts();
|
||||
fetchDispatchers();
|
||||
async function fetchAll() {
|
||||
await fetchAlerts();
|
||||
await fetchDispatchers();
|
||||
}
|
||||
|
||||
// Handle newCloudLink query param
|
||||
watch(
|
||||
() => [route.query.newCloudLink, dispatchers.value] as const,
|
||||
([newCloudLink, data]) => {
|
||||
if (newCloudLink && data?.length) {
|
||||
const id = Number(newCloudLink);
|
||||
const destination = dispatchers.value.find((d) => d.id === id);
|
||||
if (destination) {
|
||||
showDrawer(
|
||||
DestinationForm,
|
||||
{
|
||||
destination,
|
||||
onCreated: fetchDispatchers,
|
||||
existingDispatchers: dispatchers.value,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
}
|
||||
fetchAll();
|
||||
|
||||
// Handle cloudLinkSuccess hash param
|
||||
onMounted(() => {
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith("#cloudLinkSuccess=")) {
|
||||
const id = Number(hash.replace("#cloudLinkSuccess=", ""));
|
||||
if (!isNaN(id)) {
|
||||
showToast({
|
||||
title: t("notifications.cloud-link-success.title"),
|
||||
message: t("notifications.cloud-link-success.message"),
|
||||
type: "info",
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
router.replace({ hash: "" });
|
||||
}
|
||||
});
|
||||
|
||||
// Local state
|
||||
const filter = ref<"all" | "enabled" | "paused">("all");
|
||||
|
||||
@@ -16,7 +16,8 @@ export interface Dispatcher {
|
||||
type: string;
|
||||
url?: string;
|
||||
template?: string;
|
||||
apiKey?: string;
|
||||
prefix?: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface NotificationRuleInput {
|
||||
@@ -27,14 +28,6 @@ export interface NotificationRuleInput {
|
||||
containerExpression: string;
|
||||
}
|
||||
|
||||
export interface DispatcherInput {
|
||||
name: string;
|
||||
type: string;
|
||||
url?: string;
|
||||
template?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface PreviewResult {
|
||||
containerError?: string;
|
||||
logError?: string;
|
||||
|
||||
@@ -16,14 +16,16 @@ import (
|
||||
|
||||
// CloudDispatcher sends notifications to Dozzle Cloud
|
||||
type CloudDispatcher struct {
|
||||
Name string
|
||||
URL string
|
||||
APIKey string
|
||||
client *http.Client
|
||||
Name string
|
||||
URL string
|
||||
APIKey string
|
||||
Prefix string
|
||||
ExpiresAt *time.Time
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewCloudDispatcher creates a new cloud dispatcher
|
||||
func NewCloudDispatcher(name string, apiKey string) (*CloudDispatcher, error) {
|
||||
func NewCloudDispatcher(name string, apiKey string, prefix string, expiresAt *time.Time) (*CloudDispatcher, error) {
|
||||
url := os.Getenv("DOLIGENCE_URL")
|
||||
if url == "" {
|
||||
url = "https://doligence.dozzle.dev"
|
||||
@@ -35,9 +37,11 @@ func NewCloudDispatcher(name string, apiKey string) (*CloudDispatcher, error) {
|
||||
}
|
||||
|
||||
return &CloudDispatcher{
|
||||
Name: name,
|
||||
URL: url,
|
||||
APIKey: apiKey,
|
||||
Name: name,
|
||||
URL: url,
|
||||
APIKey: apiKey,
|
||||
Prefix: prefix,
|
||||
ExpiresAt: expiresAt,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
|
||||
@@ -292,10 +292,12 @@ func (m *Manager) Dispatchers() []DispatcherConfig {
|
||||
})
|
||||
case *dispatcher.CloudDispatcher:
|
||||
result = append(result, DispatcherConfig{
|
||||
ID: id,
|
||||
Name: v.Name,
|
||||
Type: "cloud",
|
||||
APIKey: v.APIKey,
|
||||
ID: id,
|
||||
Name: v.Name,
|
||||
Type: "cloud",
|
||||
APIKey: v.APIKey,
|
||||
Prefix: v.Prefix,
|
||||
ExpiresAt: v.ExpiresAt,
|
||||
})
|
||||
}
|
||||
return true
|
||||
@@ -433,12 +435,14 @@ 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,
|
||||
APIKey: d.APIKey,
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
APIKey: d.APIKey,
|
||||
Prefix: d.Prefix,
|
||||
ExpiresAt: d.ExpiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,12 +497,14 @@ func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionCon
|
||||
// Load dispatchers
|
||||
for _, dc := range dispatchers {
|
||||
d, err := createDispatcher(DispatcherConfig{
|
||||
ID: dc.ID,
|
||||
Name: dc.Name,
|
||||
Type: dc.Type,
|
||||
URL: dc.URL,
|
||||
Template: dc.Template,
|
||||
APIKey: dc.APIKey,
|
||||
ID: dc.ID,
|
||||
Name: dc.Name,
|
||||
Type: dc.Type,
|
||||
URL: dc.URL,
|
||||
Template: dc.Template,
|
||||
APIKey: dc.APIKey,
|
||||
Prefix: dc.Prefix,
|
||||
ExpiresAt: dc.ExpiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create dispatcher %s: %w", dc.Name, err)
|
||||
@@ -524,7 +530,7 @@ func createDispatcher(config DispatcherConfig) (dispatcher.Dispatcher, error) {
|
||||
case "webhook":
|
||||
return dispatcher.NewWebhookDispatcher(config.Name, config.URL, config.Template)
|
||||
case "cloud":
|
||||
return dispatcher.NewCloudDispatcher(config.Name, config.APIKey)
|
||||
return dispatcher.NewCloudDispatcher(config.Name, config.APIKey, config.Prefix, config.ExpiresAt)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown dispatcher type: %s", config.Type)
|
||||
}
|
||||
|
||||
@@ -112,12 +112,14 @@ func (s *Subscription) AddTriggeredContainer(id string) {
|
||||
|
||||
// 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
|
||||
APIKey string `json:"apiKey,omitempty" yaml:"apiKey,omitempty"` // API key for cloud dispatcher
|
||||
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
|
||||
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"`
|
||||
}
|
||||
|
||||
// Config represents the persisted notification configuration
|
||||
|
||||
+12
-2
@@ -71,9 +71,19 @@ func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var expiresAt *time.Time
|
||||
if tokenResp.ExpiresAt != nil {
|
||||
parsed, err := time.Parse(time.RFC3339, *tokenResp.ExpiresAt)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("expiresAt", *tokenResp.ExpiresAt).Msg("Failed to parse expiresAt, ignoring")
|
||||
} else {
|
||||
expiresAt = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
name := "Dozzle Cloud"
|
||||
|
||||
cloudDispatcher, err := dispatcher.NewCloudDispatcher(name, tokenResp.Key)
|
||||
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)
|
||||
@@ -86,6 +96,6 @@ func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) {
|
||||
if base == "/" {
|
||||
base = ""
|
||||
}
|
||||
redirectURL := fmt.Sprintf("%s/notifications?newCloudLink=%d", base, id)
|
||||
redirectURL := fmt.Sprintf("%s/notifications#cloudLinkSuccess=%d", base, id)
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
@@ -32,12 +32,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"`
|
||||
APIKey *string `json:"apiKey,omitempty"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
Prefix *string `json:"prefix,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationRuleInput struct {
|
||||
@@ -61,7 +62,6 @@ type DispatcherInput struct {
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
APIKey *string `json:"apiKey,omitempty"`
|
||||
}
|
||||
|
||||
type PreviewInput struct {
|
||||
@@ -125,17 +125,18 @@ func dispatcherConfigToResponse(d *notification.DispatcherConfig) *DispatcherRes
|
||||
if d.Template != "" {
|
||||
template = &d.Template
|
||||
}
|
||||
var apiKey *string
|
||||
if d.APIKey != "" {
|
||||
apiKey = &d.APIKey
|
||||
var prefix *string
|
||||
if d.Prefix != "" {
|
||||
prefix = &d.Prefix
|
||||
}
|
||||
return &DispatcherResponse{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: url,
|
||||
Template: template,
|
||||
APIKey: apiKey,
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: url,
|
||||
Template: template,
|
||||
Prefix: prefix,
|
||||
ExpiresAt: d.ExpiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,17 +343,6 @@ func (h *handler) createDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
d = webhook
|
||||
case "cloud":
|
||||
apiKey := ""
|
||||
if input.APIKey != nil {
|
||||
apiKey = *input.APIKey
|
||||
}
|
||||
cloud, err := dispatcher.NewCloudDispatcher(input.Name, apiKey)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
d = cloud
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "unknown dispatcher type")
|
||||
return
|
||||
@@ -366,7 +356,6 @@ func (h *handler) createDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
Type: input.Type,
|
||||
URL: input.URL,
|
||||
Template: input.Template,
|
||||
APIKey: input.APIKey,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -400,17 +389,6 @@ func (h *handler) updateDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
d = webhook
|
||||
case "cloud":
|
||||
apiKey := ""
|
||||
if input.APIKey != nil {
|
||||
apiKey = *input.APIKey
|
||||
}
|
||||
cloud, err := dispatcher.NewCloudDispatcher(input.Name, apiKey)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
d = cloud
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "unknown dispatcher type")
|
||||
return
|
||||
@@ -424,7 +402,6 @@ func (h *handler) updateDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
Type: input.Type,
|
||||
URL: input.URL,
|
||||
Template: input.Template,
|
||||
APIKey: input.APIKey,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -221,7 +221,7 @@ notifications:
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, custom endpoint
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Push, email, and dashboard
|
||||
cloud-description: Push notifications, email alerts, and AI-powered summaries
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API Key
|
||||
@@ -242,3 +242,8 @@ notifications:
|
||||
link-cloud: Link Account
|
||||
link-cloud-button: Link Dozzle Cloud
|
||||
cloud-linked: Your Dozzle Cloud account is linked and ready to receive notifications.
|
||||
cloud-settings-hint: To configure your managed channels, go to
|
||||
cloud-settings-link: Dozzle Cloud Settings
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Linked
|
||||
message: Your account has been successfully linked to Dozzle Cloud.
|
||||
|
||||
@@ -44,10 +44,12 @@ type SubscriptionConfig struct {
|
||||
|
||||
// DispatcherConfig represents a notification dispatcher configuration
|
||||
type DispatcherConfig struct {
|
||||
ID int
|
||||
Name string
|
||||
Type string
|
||||
URL string
|
||||
Template string
|
||||
APIKey string
|
||||
ID int
|
||||
Name string
|
||||
Type string
|
||||
URL string
|
||||
Template string
|
||||
APIKey string
|
||||
Prefix string
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user