Skip to content

Commit 74135c7

Browse files
committed
fix: add basic rendering
1 parent c944eee commit 74135c7

File tree

8 files changed

+238
-67
lines changed

8 files changed

+238
-67
lines changed

apps/api/src/app/webhooks/dtos/get-webhook-portal-token-response.dto.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ export class GetWebhookPortalTokenResponseDto {
1010
@IsString()
1111
@IsJWT()
1212
token: string;
13+
14+
@IsNotEmpty()
15+
@IsString()
16+
appId: string;
1317
}

apps/api/src/app/webhooks/usecases/get-webhook-portal-token/get-webhook-portal-token.usecase.ts

+31-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
2-
import { EnvironmentRepository } from '@novu/dal';
2+
import { EnvironmentRepository, OrganizationRepository } from '@novu/dal';
33
import { LogDecorator } from '@novu/application-generic';
44
import { Svix } from 'svix'; // Import Svix SDK type
55

@@ -14,7 +14,8 @@ const LOG_CONTEXT = 'GetWebhookPortalTokenUsecase';
1414
export class GetWebhookPortalTokenUsecase {
1515
constructor(
1616
private environmentRepository: EnvironmentRepository,
17-
@Inject('SVIX_CLIENT') private svix: Svix
17+
@Inject('SVIX_CLIENT') private svix: Svix,
18+
private organizationRepository: OrganizationRepository
1819
) {}
1920

2021
@LogDecorator()
@@ -30,34 +31,47 @@ export class GetWebhookPortalTokenUsecase {
3031
);
3132
}
3233

33-
// TODO: Refine how svixApplicationId is stored/retrieved if not in customData
34-
const svixApplicationId = environment.customData?.svixApplicationId as string;
35-
if (!svixApplicationId) {
36-
throw new NotFoundException(`Svix Application ID not configured for environment ${command.environmentId}.`);
37-
}
38-
3934
try {
40-
Logger.log(`Generating Svix portal token for app ID: ${svixApplicationId}`, LOG_CONTEXT);
41-
4235
// Call Svix SDK to get portal access URL and token
4336
const svixResponse = await this.svix.authentication.appPortalAccess(
4437
`${command.organizationId}-${command.environmentId}`,
4538
{}
4639
);
4740

48-
Logger.log(`Successfully generated Svix portal token for app ID: ${svixApplicationId}`, LOG_CONTEXT);
49-
5041
return {
5142
url: svixResponse.url,
5243
token: svixResponse.token,
44+
appId: `${command.organizationId}-${command.environmentId}`,
5345
};
5446
} catch (error) {
55-
Logger.error(
56-
`Failed to generate Svix portal token for app ID ${svixApplicationId}: ${error?.message}`,
57-
error?.stack,
58-
LOG_CONTEXT
59-
);
47+
console.log('AAAAA', error.code);
48+
if (error.code === 404) {
49+
const organization = await this.organizationRepository.findById(command.organizationId);
50+
if (!organization) {
51+
throw new NotFoundException(`Organization not found for id ${command.organizationId}`);
52+
}
53+
54+
const app = await this.svix.application.create({
55+
name: organization.name,
56+
uid: `${command.organizationId}-${command.environmentId}`,
57+
metadata: {
58+
environmentId: command.environmentId,
59+
},
60+
});
61+
62+
const svixResponse = await this.svix.authentication.appPortalAccess(
63+
`${command.organizationId}-${command.environmentId}`,
64+
{}
65+
);
66+
67+
console.log('APPsSSSs', svixResponse);
6068

69+
return {
70+
url: svixResponse.url,
71+
token: svixResponse.token,
72+
appId: `${command.organizationId}-${command.environmentId}`,
73+
};
74+
}
6175
// Re-throw or handle specific Svix errors as needed
6276
throw new Error(`Failed to generate Svix portal token: ${error?.message}`);
6377
}

apps/api/src/app/webhooks/webhooks.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { GetWebhookPortalTokenCommand } from './usecases/get-webhook-portal-toke
77
import { GetWebhookPortalTokenResponseDto } from './dtos/get-webhook-portal-token-response.dto';
88
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
99

10-
@Controller({ path: `/webhooks`, version: '1' })
10+
@Controller({ path: `/webhooks`, version: '2' })
1111
@UseInterceptors(ClassSerializerInterceptor)
1212
@UserAuthentication()
1313
export class WebhooksController {

apps/dashboard/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"react-timezone-select": "^3.2.8",
102102
"react-use-intercom": "^2.0.0",
103103
"sonner": "^1.7.0",
104+
"svix-react": "^1.13.3",
104105
"tailwind-merge": "^2.4.0",
105106
"tailwind-variants": "^0.3.0",
106107
"tailwindcss-animate": "^1.0.7",

apps/dashboard/src/api/webhooks.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { IEnvironment } from '@novu/shared';
2+
import { getV2 } from './api.client';
3+
4+
// Matches the response DTO defined in the API
5+
interface GetWebhookPortalTokenResponse {
6+
url: string;
7+
token: string;
8+
}
9+
10+
export const getWebhookPortalToken = async (environment: IEnvironment): Promise<GetWebhookPortalTokenResponse> => {
11+
const { data } = await getV2<{ data: GetWebhookPortalTokenResponse }>('/webhooks/portal/token', {
12+
environment,
13+
});
14+
15+
return data;
16+
};

apps/dashboard/src/index.css

+4
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,7 @@
367367
-ms-overflow-style: none; /* IE and Edge */
368368
scrollbar-width: none; /* Firefox */
369369
}
370+
371+
iframe #navigation-bar {
372+
display: none;
373+
}

apps/dashboard/src/pages/webhooks-page.tsx

+96-36
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,115 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitive
44
import { DashboardLayout } from '../components/dashboard-layout';
55
import { Navigate } from 'react-router-dom';
66
import { ROUTES } from '@/utils/routes';
7+
import { useEffect, useState } from 'react';
8+
import { useEnvironment } from '@/context/environment/hooks';
9+
import { getWebhookPortalToken } from '@/api/webhooks';
10+
import { AppPortal, SvixProvider } from 'svix-react';
711

812
export function WebhooksPage() {
913
const isWebhooksManagementEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_WEBHOOKS_MANAGEMENT_ENABLED);
14+
const { currentEnvironment } = useEnvironment();
15+
const [portalUrl, setPortalUrl] = useState<string | null>(null);
16+
const [appId, setAppId] = useState<string | null>(null);
17+
const [portalToken, setPortalToken] = useState<string | null>(null);
18+
const [isLoading, setIsLoading] = useState<boolean>(true);
19+
const [error, setError] = useState<string | null>(null);
20+
21+
useEffect(() => {
22+
if (!isWebhooksManagementEnabled || !currentEnvironment) {
23+
setIsLoading(false);
24+
25+
return;
26+
}
27+
28+
const fetchToken = async () => {
29+
setIsLoading(true);
30+
setError(null);
31+
32+
try {
33+
const response = await getWebhookPortalToken(currentEnvironment);
34+
setPortalUrl(response.url);
35+
setPortalToken(response.token);
36+
setAppId(response.appId);
37+
console.log('Webhook Portal URL:', response.url);
38+
console.log('Webhook Portal Token:', response.token);
39+
} catch (e: any) {
40+
console.error('Failed to fetch webhook portal token:', e);
41+
setError(e.message || 'Failed to load portal token.');
42+
} finally {
43+
setIsLoading(false);
44+
}
45+
};
46+
47+
fetchToken();
48+
}, [isWebhooksManagementEnabled, currentEnvironment]);
1049

1150
if (!isWebhooksManagementEnabled) {
1251
// Or redirect to a 'Not Found' page or a parent page
1352

1453
return <Navigate to={ROUTES.WORKFLOWS} replace />;
1554
}
1655

56+
// TODO: Add UI elements to display loading/error states and potentially the URL/token if needed
57+
58+
if (!portalToken || !appId) {
59+
return <div>No portal token found</div>;
60+
}
61+
62+
console.log({ portalUrl });
63+
64+
// Function to safely construct the portal URL with the next parameter
65+
const buildPortalUrl = (baseUrl: string | null, nextPath: string): string => {
66+
if (!baseUrl) return '';
67+
const urlParts = baseUrl.split('#');
68+
69+
if (urlParts.length !== 2) {
70+
console.error('Unexpected Svix portal URL format:', baseUrl);
71+
return baseUrl;
72+
}
73+
74+
const base = urlParts[0];
75+
const keyFragment = urlParts[1];
76+
// Add only the next parameter
77+
const separator = base.includes('?') ? '&' : '?';
78+
return `${base}${separator}next=${nextPath}#${keyFragment}`;
79+
};
80+
1781
return (
1882
<DashboardLayout headerStartItems={<h1 className="text-foreground-950">Webhooks</h1>}>
19-
<Tabs defaultValue="endpoints">
20-
<div className="border-neutral-alpha-200 flex items-center justify-between border-b">
21-
<TabsList variant="regular" className="border-b-0 border-t-2 border-transparent p-0 !px-2">
22-
<TabsTrigger value="endpoints" variant="regular">
23-
Endpoints
24-
</TabsTrigger>
25-
<TabsTrigger value="event-catalog" variant="regular">
26-
Event Catalog
27-
</TabsTrigger>
28-
<TabsTrigger value="logs" variant="regular">
29-
Logs
30-
</TabsTrigger>
31-
<TabsTrigger value="activity" variant="regular">
32-
Activity
33-
</TabsTrigger>
34-
</TabsList>
35-
{/* Add action buttons here if needed later */}
36-
</div>
37-
<TabsContent value="endpoints" variant="regular" className="!mt-0 p-2.5">
38-
<div className="text-muted-foreground flex h-64 items-center justify-center">
39-
Endpoints Content Placeholder
40-
</div>
41-
</TabsContent>
42-
<TabsContent value="event-catalog" variant="regular" className="!mt-0 p-2.5">
43-
<div className="text-muted-foreground flex h-64 items-center justify-center">
44-
Event Catalog Content Placeholder
45-
</div>
46-
</TabsContent>
47-
<TabsContent value="logs" variant="regular" className="!mt-0 p-2.5">
48-
<div className="text-muted-foreground flex h-64 items-center justify-center">Logs Content Placeholder</div>
49-
</TabsContent>
50-
<TabsContent value="activity" variant="regular" className="!mt-0 p-2.5">
51-
<div className="text-muted-foreground flex h-64 items-center justify-center">
52-
Activity Content Placeholder
83+
<SvixProvider token={portalToken} appId={appId}>
84+
<Tabs defaultValue="endpoints">
85+
<div className="border-neutral-alpha-200 flex items-center justify-between border-b">
86+
<TabsList variant="regular" className="border-b-0 border-t-2 border-transparent p-0 !px-2">
87+
<TabsTrigger value="endpoints" variant="regular">
88+
Endpoints
89+
</TabsTrigger>
90+
<TabsTrigger value="event-catalog" variant="regular">
91+
Event Catalog
92+
</TabsTrigger>
93+
<TabsTrigger value="logs" variant="regular">
94+
Logs
95+
</TabsTrigger>
96+
<TabsTrigger value="activity" variant="regular">
97+
Activity
98+
</TabsTrigger>
99+
</TabsList>
100+
{/* Add action buttons here if needed later */}
53101
</div>
54-
</TabsContent>
55-
</Tabs>
102+
<TabsContent value="endpoints" variant="regular" className="!mt-0 p-2.5">
103+
<AppPortal url={buildPortalUrl(portalUrl, '/endpoints')} style={{ backgroundColor: 'red' }} fullSize />
104+
</TabsContent>
105+
<TabsContent value="event-catalog" variant="regular" className="!mt-0 p-2.5">
106+
<AppPortal url={buildPortalUrl(portalUrl, '/event-types')} fullSize />
107+
</TabsContent>
108+
<TabsContent value="logs" variant="regular" className="!mt-0 p-2.5">
109+
<AppPortal url={buildPortalUrl(portalUrl, '/messages')} fullSize />
110+
</TabsContent>
111+
<TabsContent value="activity" variant="regular" className="!mt-0 p-2.5">
112+
<AppPortal url={buildPortalUrl(portalUrl, '/activity')} fullSize />
113+
</TabsContent>
114+
</Tabs>
115+
</SvixProvider>
56116
</DashboardLayout>
57117
);
58118
}

0 commit comments

Comments
 (0)