Skip to content

Commit e67760a

Browse files
authored
Merge pull request #667 from kubero-dev/feature/migrate-notifications-to-db
Migrate notifications to Database
2 parents b874b2f + a887967 commit e67760a

12 files changed

Lines changed: 1117 additions & 20 deletions

File tree

client/src/components/notifications/index.vue

Lines changed: 556 additions & 0 deletions
Large diffs are not rendered by default.

client/src/layouts/default/NavDrawer.vue

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,43 @@
5656
title="Accounts">
5757
</v-list-item>
5858
<!-- Posponed for later release
59-
<v-list-item
60-
link to="/runpacks"
61-
v-if="kubero.isAuthenticated && !kubero.adminDisabled"
62-
prepend-icon="mdi-cube-outline"
63-
title="Runpacks">
64-
</v-list-item>
6559
-->
66-
<v-list-item
67-
link to="/settings"
68-
v-if="kubero.isAuthenticated && !kubero.adminDisabled && (authStore.hasPermission('config:write') || authStore.hasPermission('config:read'))"
69-
prepend-icon="mdi-cog-outline"
70-
title="Settings">
71-
</v-list-item>
60+
<!-- Settings subsection -->
61+
<v-list-group
62+
v-if="kubero.isAuthenticated && !kubero.adminDisabled && (authStore.hasPermission('config:write') || authStore.hasPermission('config:read'))"
63+
v-model="settingsOpen"
64+
prepend-icon="mdi-cog-outline"
65+
value="settings"
66+
title="Settings"
67+
>
68+
<template #activator="{ props }">
69+
<v-list-item v-bind="props" title="Settings"></v-list-item>
70+
</template>
71+
<v-list-item
72+
link to="/settings"
73+
title="General"
74+
prepend-icon="mdi-tune"
75+
density="compact"
76+
></v-list-item>
77+
<v-list-item
78+
link to="/runpacks"
79+
v-if="kubero.isAuthenticated && !kubero.adminDisabled"
80+
prepend-icon="mdi-cube-outline"
81+
title="Runpacks">
82+
</v-list-item>
83+
<v-list-item
84+
link to="/podsizes"
85+
v-if="kubero.isAuthenticated && !kubero.adminDisabled"
86+
prepend-icon="mdi-arrow-expand-vertical"
87+
title="Pod Sizes">
88+
</v-list-item>
89+
<v-list-item
90+
link to="/notifications"
91+
v-if="kubero.isAuthenticated && !kubero.adminDisabled"
92+
prepend-icon="mdi-email-fast-outline"
93+
title="Notifications">
94+
</v-list-item>
95+
</v-list-group>
7296
</v-list>
7397

7498

@@ -275,7 +299,8 @@ export default defineComponent({
275299
version: '0.0.1',
276300
templatesEnabled: false,
277301
session: false,
278-
debugDialog: false
302+
debugDialog: false,
303+
settingsOpen: false // Controls collapse state
279304
}
280305
},
281306
computed: {

client/src/router/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ const routes = [
130130
},
131131
],
132132
},
133+
{
134+
path: '/notifications',
135+
component: () => import('@/layouts/default/Default.vue'),
136+
children: [
137+
{
138+
path: '/notifications',
139+
name: 'Notifications',
140+
component: () => import('@/views/Notifications.vue'),
141+
},
142+
],
143+
},
133144
{
134145
path: '/login',
135146
component: () => import('@/layouts/login/Login.vue'),

client/src/views/Notifications.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<template>
2+
<NotificationsList />
3+
</template>
4+
5+
<script lang="ts" setup>
6+
import NotificationsList from '@/components/notifications/index.vue'
7+
</script>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- CreateTable
2+
CREATE TABLE "Notification" (
3+
"id" TEXT NOT NULL PRIMARY KEY,
4+
"name" TEXT NOT NULL,
5+
"enabled" BOOLEAN NOT NULL DEFAULT true,
6+
"type" TEXT NOT NULL,
7+
"pipelines" TEXT NOT NULL,
8+
"events" TEXT NOT NULL,
9+
"webhookUrl" TEXT,
10+
"webhookSecret" TEXT,
11+
"slackUrl" TEXT,
12+
"slackChannel" TEXT,
13+
"discordUrl" TEXT,
14+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
15+
"updatedAt" DATETIME NOT NULL
16+
);

server/prisma/schema.prisma

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,30 @@ model CapabilityDrop {
234234
value String
235235
capability Capability @relation(fields: [capabilityId], references: [id])
236236
capabilityId String
237+
}
238+
239+
// NOTIFICATIONS
240+
model Notification {
241+
id String @id @default(cuid())
242+
name String
243+
enabled Boolean @default(true)
244+
type NotificationType
245+
pipelines String // JSON string array of pipeline names or "all"
246+
events String // JSON string array of event names
247+
248+
// Configuration fields for different notification types
249+
webhookUrl String?
250+
webhookSecret String?
251+
slackUrl String?
252+
slackChannel String?
253+
discordUrl String?
254+
255+
createdAt DateTime @default(now())
256+
updatedAt DateTime @updatedAt
257+
}
258+
259+
enum NotificationType {
260+
slack
261+
webhook
262+
discord
237263
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { PrismaClient } from '@prisma/client';
3+
import { INotificationConfig } from './notifications.interface';
4+
5+
// Define our own types since Prisma client isn't generated yet
6+
export interface NotificationDb {
7+
id: string;
8+
name: string;
9+
enabled: boolean;
10+
type: 'slack' | 'webhook' | 'discord';
11+
pipelines: string; // JSON string
12+
events: string; // JSON string
13+
webhookUrl?: string | null;
14+
webhookSecret?: string | null;
15+
slackUrl?: string | null;
16+
slackChannel?: string | null;
17+
discordUrl?: string | null;
18+
createdAt: Date;
19+
updatedAt: Date;
20+
}
21+
22+
export interface CreateNotificationDto {
23+
name: string;
24+
enabled: boolean;
25+
type: 'slack' | 'webhook' | 'discord';
26+
pipelines: string[];
27+
events: string[];
28+
config: {
29+
url?: string;
30+
secret?: string;
31+
channel?: string;
32+
};
33+
}
34+
35+
export interface UpdateNotificationDto extends Partial<CreateNotificationDto> {
36+
id: string;
37+
}
38+
39+
@Injectable()
40+
export class NotificationsDbService {
41+
private readonly logger = new Logger(NotificationsDbService.name);
42+
private readonly prisma = new PrismaClient();
43+
44+
async findAll(): Promise<NotificationDb[]> {
45+
return await this.prisma.notification.findMany({
46+
orderBy: { createdAt: 'desc' },
47+
}) as NotificationDb[];
48+
}
49+
50+
async findById(id: string): Promise<NotificationDb | null> {
51+
return await this.prisma.notification.findUnique({
52+
where: { id },
53+
}) as NotificationDb | null;
54+
}
55+
56+
async create(data: CreateNotificationDto): Promise<NotificationDb> {
57+
const notificationData: any = {
58+
name: data.name,
59+
enabled: data.enabled,
60+
type: data.type,
61+
pipelines: JSON.stringify(data.pipelines),
62+
events: JSON.stringify(data.events),
63+
};
64+
65+
// Map config fields based on notification type
66+
switch (data.type) {
67+
case 'webhook':
68+
notificationData.webhookUrl = data.config.url;
69+
notificationData.webhookSecret = data.config.secret;
70+
break;
71+
case 'slack':
72+
notificationData.slackUrl = data.config.url;
73+
notificationData.slackChannel = data.config.channel;
74+
break;
75+
case 'discord':
76+
notificationData.discordUrl = data.config.url;
77+
break;
78+
}
79+
80+
const notification = await this.prisma.notification.create({
81+
data: notificationData,
82+
}) as NotificationDb;
83+
84+
this.logger.log(`Notification '${notification.name}' created successfully`);
85+
return notification;
86+
}
87+
88+
async update(id: string, data: Partial<CreateNotificationDto>): Promise<NotificationDb> {
89+
const updateData: any = {};
90+
91+
if (data.name !== undefined) updateData.name = data.name;
92+
if (data.enabled !== undefined) updateData.enabled = data.enabled;
93+
if (data.type !== undefined) updateData.type = data.type;
94+
if (data.pipelines !== undefined) updateData.pipelines = JSON.stringify(data.pipelines);
95+
if (data.events !== undefined) updateData.events = JSON.stringify(data.events);
96+
97+
// Clear existing config fields and set new ones
98+
if (data.config && data.type) {
99+
// Clear all config fields first
100+
updateData.webhookUrl = null;
101+
updateData.webhookSecret = null;
102+
updateData.slackUrl = null;
103+
updateData.slackChannel = null;
104+
updateData.discordUrl = null;
105+
106+
// Set appropriate fields based on type
107+
switch (data.type) {
108+
case 'webhook':
109+
updateData.webhookUrl = data.config.url;
110+
updateData.webhookSecret = data.config.secret;
111+
break;
112+
case 'slack':
113+
updateData.slackUrl = data.config.url;
114+
updateData.slackChannel = data.config.channel;
115+
break;
116+
case 'discord':
117+
updateData.discordUrl = data.config.url;
118+
break;
119+
}
120+
}
121+
122+
const notification = await this.prisma.notification.update({
123+
where: { id },
124+
data: updateData,
125+
}) as NotificationDb;
126+
127+
this.logger.log(`Notification '${notification.name}' updated successfully`);
128+
return notification;
129+
}
130+
131+
async delete(id: string): Promise<void> {
132+
const notification = await this.prisma.notification.findUnique({
133+
where: { id },
134+
}) as NotificationDb | null;
135+
136+
if (!notification) {
137+
throw new Error(`Notification with id ${id} not found`);
138+
}
139+
140+
await this.prisma.notification.delete({
141+
where: { id },
142+
});
143+
144+
this.logger.log(`Notification '${notification.name}' deleted successfully`);
145+
}
146+
147+
// Convert database notification to the format expected by the notification service
148+
toNotificationConfig(notification: NotificationDb): INotificationConfig {
149+
const config: any = {};
150+
151+
switch (notification.type) {
152+
case 'webhook':
153+
config.url = notification.webhookUrl;
154+
config.secret = notification.webhookSecret;
155+
break;
156+
case 'slack':
157+
config.url = notification.slackUrl;
158+
config.channel = notification.slackChannel;
159+
break;
160+
case 'discord':
161+
config.url = notification.discordUrl;
162+
break;
163+
}
164+
165+
return {
166+
id: notification.id,
167+
enabled: notification.enabled,
168+
name: notification.name,
169+
type: notification.type as 'slack' | 'webhook' | 'discord',
170+
pipelines: JSON.parse(notification.pipelines),
171+
events: JSON.parse(notification.events),
172+
config,
173+
};
174+
}
175+
176+
// Get all notifications in the format expected by the notification service
177+
async getNotificationConfigs(): Promise<INotificationConfig[]> {
178+
const notifications = await this.findAll();
179+
return notifications.map(notification => this.toNotificationConfig(notification));
180+
}
181+
182+
// Migration helper: Create notifications from YAML config
183+
async migrateFromConfig(configNotifications: INotificationConfig[]): Promise<void> {
184+
this.logger.log('Starting migration of notifications from YAML config to database');
185+
186+
for (const configNotification of configNotifications) {
187+
try {
188+
const existingNotification = await this.prisma.notification.findFirst({
189+
where: { name: configNotification.name },
190+
});
191+
192+
if (existingNotification) {
193+
this.logger.warn(`Notification '${configNotification.name}' already exists in database, skipping`);
194+
continue;
195+
}
196+
197+
await this.create({
198+
name: configNotification.name,
199+
enabled: configNotification.enabled,
200+
type: configNotification.type as 'slack' | 'webhook' | 'discord',
201+
pipelines: configNotification.pipelines,
202+
events: configNotification.events,
203+
config: configNotification.config as any,
204+
});
205+
206+
this.logger.log(`Migrated notification '${configNotification.name}' to database`);
207+
} catch (error) {
208+
this.logger.error(`Failed to migrate notification '${configNotification.name}': ${error.message}`);
209+
}
210+
}
211+
212+
this.logger.log('Completed migration of notifications from YAML config to database');
213+
}
214+
}

0 commit comments

Comments
 (0)