Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion backend/src/components/utils/categorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface ComponentCategoryConfig {
icon: string;
}

const SUPPORTED_CATEGORIES: ReadonlyArray<ComponentCategory> = ['input', 'transform', 'ai', 'security', 'it_ops', 'output'];
const SUPPORTED_CATEGORIES: ReadonlyArray<ComponentCategory> = ['input', 'transform', 'ai', 'security', 'it_ops', 'notification', 'output'];

const COMPONENT_CATEGORY_CONFIG: Record<ComponentCategory, ComponentCategoryConfig> = {
input: {
Expand Down Expand Up @@ -46,6 +46,13 @@ const COMPONENT_CATEGORY_CONFIG: Record<ComponentCategory, ComponentCategoryConf
emoji: '🏢',
icon: 'Building',
},
notification: {
label: 'Notification',
color: 'text-pink-600',
description: 'Slack, Email, and other messaging alerts',
emoji: '🔔',
icon: 'Bell',
},
output: {
label: 'Output',
color: 'text-green-600',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
ai: 'text-purple-600 dark:text-purple-400',
security: 'text-red-600 dark:text-red-400',
it_ops: 'text-cyan-600 dark:text-cyan-400',
notification: 'text-pink-600 dark:text-pink-400',
output: 'text-green-600 dark:text-green-400',
}
return categoryColors[category] || 'text-foreground'
Expand Down Expand Up @@ -316,7 +317,7 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
}, [filteredComponents])

// Category display order
const categoryOrder = ['input', 'output', 'security', 'ai', 'transform', 'it_ops'] as const
const categoryOrder = ['input', 'output', 'notification', 'security', 'ai', 'transform', 'it_ops'] as const

// Filter components based on search query
const filteredComponentsByCategory = useMemo(() => {
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/workflow/ParameterField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1043,8 +1043,14 @@ export function ParameterFieldWrapper({
)
}

// Special case: Logic Script Variables
if (componentId === 'core.logic.script' && (parameter.id === 'variables' || parameter.id === 'returns')) {
// Special case: Logic Script Variables & Slack Notification Variables
const isLogicScript = componentId === 'core.logic.script'
const isSlackNotification = componentId === 'core.notification.slack'

if (
(isLogicScript && (parameter.id === 'variables' || parameter.id === 'returns')) ||
(isSlackNotification && parameter.id === 'variables')
) {
const isInput = parameter.id === 'variables'
const title = isInput ? 'Input Variables' : 'Return Variables'

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/schemas/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const ComponentMetadataSchema = z.object({
name: z.string().min(1),
version: z.string().default('1.0.0'),
type: z.enum(['trigger', 'input', 'scan', 'process', 'output']),
category: z.enum(['input', 'transform', 'ai', 'security', 'it_ops', 'output']),
category: z.enum(['input', 'transform', 'ai', 'security', 'it_ops', 'notification', 'output']),
categoryConfig: ComponentCategoryConfigSchema.optional().default({
label: 'Uncategorized',
color: 'text-muted-foreground',
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/utils/categoryColors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useThemeStore } from '@/store/themeStore'

// Component category type
export type ComponentCategory = 'input' | 'transform' | 'ai' | 'security' | 'it_ops' | 'output'
export type ComponentCategory = 'input' | 'transform' | 'ai' | 'security' | 'it_ops' | 'notification' | 'output'

/**
* Category-based separator colors (2 shades lighter than normal)
Expand All @@ -28,6 +28,10 @@ export const CATEGORY_SEPARATOR_COLORS: Record<ComponentCategory, { light: strin
light: 'rgb(103 232 249)', // cyan-300 (2 shades lighter than cyan-500)
dark: 'rgb(103 232 249)', // cyan-300 (2 shades lighter than cyan-400)
},
notification: {
light: 'rgb(249 168 212)', // pink-300
dark: 'rgb(249 168 212)', // pink-300
},
output: {
light: 'rgb(134 239 172)', // green-300 (2 shades lighter than green-500)
dark: 'rgb(134 239 172)', // green-300 (2 shades lighter than green-400)
Expand Down Expand Up @@ -59,6 +63,10 @@ export const CATEGORY_HEADER_BG_COLORS: Record<ComponentCategory, { light: strin
light: 'rgb(250 254 255)', // custom cyan-25
dark: 'rgb(22 78 99 / 0.15)', // cyan-950/15
},
notification: {
light: 'rgb(255 250 253)', // pink-25
dark: 'rgb(80 7 36 / 0.15)', // pink-950/15
},
output: {
light: 'rgb(250 255 250)', // custom green-25
dark: 'rgb(20 83 45 / 0.15)', // green-950/15
Expand Down
1 change: 1 addition & 0 deletions packages/component-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export type ComponentCategory =
| 'ai'
| 'security'
| 'it_ops'
| 'notification'
| 'output';

export type ComponentUiType =
Expand Down
1 change: 1 addition & 0 deletions worker/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import './core/entry-point';
import './core/file-loader';
import './core/http-request';
import './core/logic-script';
import './notification/slack';
import './core/text-splitter';
import './core/text-joiner';
import './core/console-log';
Expand Down
89 changes: 89 additions & 0 deletions worker/src/components/notification/slack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test';
import { definition } from './slack';

const originalFetch = global.fetch;

describe('Slack Component Template Support', () => {
const mockContext = {
logger: {
info: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
debug: mock(() => {}),
},
} as any;

beforeEach(() => {
mock.restore();
});

afterEach(() => {
global.fetch = originalFetch;
});

it('should interpolate text and blocks with dynamic variables', async () => {
const globalFetch = mock(() => Promise.resolve(new Response(JSON.stringify({ ok: true, ts: '123' }), { status: 200 })));
global.fetch = globalFetch;

const params = {
authType: 'bot_token' as const,
slackToken: 'xoxb-test',
channel: 'C1',
text: 'Alert: {{severity}} issue on {{host}}',
blocks: [
{
type: 'section',
text: { type: 'mrkdwn', text: '*System:* {{host}}' }
}
],
host: 'prod-db-01',
severity: 'CRITICAL'
};

const result = await definition.execute(params, mockContext);

expect(result.ok).toBe(true);
const body = JSON.parse(globalFetch.mock.calls[0][1].body);

// Check text interpolation
expect(body.text).toBe('Alert: CRITICAL issue on prod-db-01');

// Check blocks interpolation
expect(body.blocks[0].text.text).toBe('*System:* prod-db-01');
});

it('should handle JSON string blocks template', async () => {
const globalFetch = mock(() => Promise.resolve(new Response(JSON.stringify({ ok: true }), { status: 200 })));
global.fetch = globalFetch;

const params = {
authType: 'webhook' as const,
webhookUrl: 'https://webhook',
text: 'Plain text',
blocks: '[{"type": "section", "text": {"type": "plain_text", "text": "{{user}} joined" }}]',
user: 'Alice'
};

await definition.execute(params, mockContext);

const body = JSON.parse(globalFetch.mock.calls[0][1].body);
expect(body.blocks[0].text.text).toBe('Alice joined');
});

it('should resolve dynamic ports for variables', () => {
const ports = definition.resolvePorts!({
authType: 'bot_token',
variables: [
{ name: 'error_msg', type: 'string' },
{ name: 'timestamp', type: 'string' }
]
});

const errorPort = ports.inputs!.find(i => i.id === 'error_msg');
const tsPort = ports.inputs!.find(i => i.id === 'timestamp');

expect(errorPort).toBeDefined();
expect(tsPort).toBeDefined();
expect(errorPort?.label).toBe('error_msg');
});
});
Loading