Skip to content

Commit 99b60f8

Browse files
authored
fix(dashboard): route templated endpoint test events through dynamic api (#2693)
* fix(dashboard): route templated endpoint test events through dynamic api Send test event from the endpoints list previously posted every test to /events/dynamic. That misbinds concrete endpoints (the dynamic worker resolves by URL match and can mint an orphan endpoint with fresh secrets when the URL does not match exactly) and read only endpoint.url, which is empty under the dashboard version header that renames it to target_url. Now only templated endpoints use the dynamic path, and only when both the license and the org feature flag are on (mirroring the backend gate), so the worker runs template matching instead of orphan-creating. Concrete endpoints keep the endpoint-bound /events path tied to the selected endpoint. The flag check kicked off in ngOnInit is awaited before routing so a fast click cannot misroute a templated endpoint. Also surface url templates in the UI: a shared urlTemplateParts pipe tints {token} segments in the endpoints list (feature-gated) and event delivery details, the resolved target_url is shown on delivery details, and copy affordances plus a url-template tooltip were added. * fix(dashboard): group url fallback before urlTemplateParts pipe The pipe binds tighter than ||, so `endpoint.url || endpoint.target_url | urlTemplateParts` parsed as `endpoint.url || (target_url | pipe)`. When endpoint.url was set the @for iterated a raw string and the url column rendered blank. Parenthesize the fallback so the pipe receives the resolved url. * fix(controlplane): stop dynamic events from overwriting subscription filters CreateDynamicEventService forced an omitted event_types list to ["*"], and the worker's syncDynamicSubscriptionEventTypes then persisted that onto the matched endpoint's existing subscription. A dynamic event without event_types (e.g. the dashboard "Send test event" for a templated endpoint) silently widened a restrictive filter to all event types. Keep an omitted list empty through to the worker so empty means "do not change the filter": existing subscriptions are left untouched, and only a brand-new dynamic subscription defaults to ["*"] so future non-dynamic events still match. Explicit event_types still sync as before. The dynamic channel bypasses filter matching, so test delivery is unaffected for restrictive filters. Adds tests: existing filter preserved on empty event_types, and new subscription defaults to catch-all.
1 parent 071c58f commit 99b60f8

11 files changed

Lines changed: 195 additions & 24 deletions

File tree

services/create_dynamic_event.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ func (e *CreateDynamicEventService) Run(ctx context.Context) (err error) {
4646
e.DynamicEvent.ProjectID = e.Project.UID
4747
e.DynamicEvent.AcknowledgedAt = time.Now()
4848

49-
if len(e.DynamicEvent.EventTypes) == 0 {
50-
e.DynamicEvent.EventTypes = []string{"*"}
51-
}
49+
// Do not default EventTypes to ["*"] here. An empty list must reach the worker as
50+
// empty so it can distinguish "caller did not specify a filter" (leave an existing
51+
// subscription's filter untouched, and default only a brand-new subscription to
52+
// catch-all) from "caller explicitly set a filter" (sync it onto the subscription).
53+
// Defaulting here would make every dynamic event that omits event_types silently
54+
// overwrite the matched subscription's filter with ["*"].
5255

5356
taskName := convoy.CreateDynamicEventProcessor
5457

web/ui/dashboard/src/app/models/event.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface EVENT_DELIVERY {
5555
device_metadata: DEVICE;
5656
endpoint_id: string;
5757
latency_seconds?: number;
58+
target_url?: string;
5859
}
5960

6061
export interface EVENT_DELIVERY_ATTEMPT {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Pipe, PipeTransform } from '@angular/core';
2+
3+
export interface UrlTemplatePart {
4+
text: string;
5+
token: boolean;
6+
}
7+
8+
// Splits a URL into segments so templated tokens like {reference} can be rendered
9+
// distinctly from the static parts of the URL. Token names follow the backend
10+
// endpoint URL template rules: {name} where name is [A-Za-z_][A-Za-z0-9_]*.
11+
@Pipe({
12+
name: 'urlTemplateParts',
13+
standalone: true
14+
})
15+
export class UrlTemplatePartsPipe implements PipeTransform {
16+
transform(value?: string | null): UrlTemplatePart[] {
17+
if (!value) return [];
18+
return value
19+
.split(/(\{[A-Za-z_][A-Za-z0-9_]*\})/g)
20+
.filter(part => part !== '')
21+
.map(part => ({ text: part, token: /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(part) }));
22+
}
23+
}

web/ui/dashboard/src/app/private/components/create-endpoint/create-endpoint.component.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@
1111
</convoy-input-field>
1212

1313
<convoy-input-field>
14-
<label for="endpoint_url" convoy-label required="true">Enter URL</label>
14+
<label for="endpoint_url" convoy-label required="true" class="flex items-center">Enter URL@if (endpointURLTemplatesFeatureEnabled) {<convoy-tooltip size="sm" position="top-right" class="ml-4px" tooltipContent="Use single-segment placeholders like {reference} in the path or query for dynamic callback URLs."></convoy-tooltip>}</label>
1515
<input type="url" id="endpoint_url" convoy-input autocomplete="url" formControlName="url" [placeholder]="endpointURLTemplatesFeatureEnabled ? 'https://example.com/orders/{reference}/callback' : 'Enter endpoint URL here'" />
16-
@if (endpointURLTemplatesFeatureEnabled) {
17-
<p class="text-10 text-neutral-10 mt-6px">Use single-segment placeholders like <code>{{ '{' }}reference{{ '}' }}</code> in the path or query for dynamic callback URLs.</p>
18-
}
1916
@if (addNewEndpointForm.get('url')?.touched && addNewEndpointForm.get('url')?.invalid) {
2017
<convoy-input-error>Invalid endpoint URL</convoy-input-error>
2118
}

web/ui/dashboard/src/app/private/pages/project/endpoints/endpoints.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ <h1 class="text-18 font-bold text-neutral-12 mr-10px">Endpoints</h1>
8383
</td>
8484
<td convoy-table-cell class="relative">
8585
<div convoy-tag class="!gap-10px flex items-center">
86-
<span class="w-[200px] truncate">{{ endpoint.url || endpoint.target_url }}</span>
86+
<span class="w-[200px] overflow-x-auto whitespace-nowrap">@if (endpointURLTemplatesFeatureEnabled) {@for (part of ((endpoint.url || endpoint.target_url) | urlTemplateParts); track $index) {<span [class]="part.token ? 'text-new.primary-400 font-medium bg-new.primary-25 rounded-[3px] px-[3px]' : ''">{{ part.text }}</span>}} @else {{{ endpoint.url || endpoint.target_url }}}</span>
8787
<convoy-copy-button size="sm" [notificationText]="endpoint.name || endpoint.title + ' Url has been copied to clipboard'" [text]="endpoint.url || endpoint.target_url"></convoy-copy-button>
8888
</div>
8989
</td>

web/ui/dashboard/src/app/private/pages/project/endpoints/endpoints.component.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { EndpointSecretComponent } from './endpoint-secret/endpoint-secret.compo
2424
import { EndpointsService } from './endpoints.service';
2525
import { LoaderModule } from 'src/app/private/components/loader/loader.module';
2626
import { LicensesService } from '../../../../services/licenses/licenses.service';
27+
import { SettingsService } from '../../settings/settings.service';
28+
import { UrlTemplatePartsPipe } from 'src/app/pipes/url-template-parts/url-template-parts.pipe';
2729

2830
@Component({
2931
selector: 'convoy-endpoints',
@@ -52,7 +54,8 @@ import { LicensesService } from '../../../../services/licenses/licenses.service'
5254
EndpointSecretComponent,
5355
DeleteModalComponent,
5456
LoaderModule,
55-
DialogDirective
57+
DialogDirective,
58+
UrlTemplatePartsPipe
5659
],
5760
templateUrl: './endpoints.component.html',
5861
styleUrls: ['./endpoints.component.scss']
@@ -76,8 +79,10 @@ export class EndpointsComponent implements OnInit {
7679
endpointSearchString!: string;
7780
action: 'create' | 'update' = 'create';
7881
userSearch = false;
82+
endpointURLTemplatesFeatureEnabled = false;
83+
private featureFlagReady?: Promise<void>;
7984

80-
constructor(public router: Router, public privateService: PrivateService, public projectService: ProjectService, private endpointService: EndpointsService, private generalService: GeneralService, public route: ActivatedRoute, public licenseService: LicensesService) {}
85+
constructor(public router: Router, public privateService: PrivateService, public projectService: ProjectService, private endpointService: EndpointsService, private generalService: GeneralService, public route: ActivatedRoute, public licenseService: LicensesService, private settingsService: SettingsService) {}
8186

8287
ngOnInit() {
8388
const urlParam = this.route.snapshot.params.id;
@@ -87,9 +92,26 @@ export class EndpointsComponent implements OnInit {
8792
}
8893
this.endpointsTableHead[4] = this.licenseService.hasLicense('CircuitBreaking') ? 'Failure Rate' : '';
8994

95+
this.featureFlagReady = this.checkEndpointURLTemplatesFeatureFlag();
9096
this.getEndpoints();
9197
}
9298

99+
async checkEndpointURLTemplatesFeatureFlag() {
100+
// Only the org-scoped early-adopter feature flag is checked here; the license
101+
// side is verified separately in sendTestEvent. Both must hold for the backend
102+
// to run template matching, so we mirror that before using the dynamic path.
103+
const org = localStorage.getItem('CONVOY_ORG');
104+
if (!org) return;
105+
try {
106+
this.endpointURLTemplatesFeatureEnabled = await this.settingsService.checkFeatureFlagEnabled({
107+
org_id: JSON.parse(org).uid,
108+
feature_key: 'endpoint-url-templates'
109+
});
110+
} catch {
111+
this.endpointURLTemplatesFeatureEnabled = false;
112+
}
113+
}
114+
93115
async getEndpoints(requestDetails?: CURSOR & { search?: string; hideLoader?: boolean }) {
94116
this.isLoadingEndpoints = !requestDetails?.hideLoader;
95117
this.userSearch = !!requestDetails?.search;
@@ -162,22 +184,59 @@ export class EndpointsComponent implements OnInit {
162184
}
163185

164186
async sendTestEvent() {
165-
const testEvent = {
166-
data: { data: 'test event from Convoy', convoy: 'https://getconvoy.io', amount: 1000 },
167-
endpoint_id: this.selectedEndpoint?.uid,
168-
event_type: 'test.convoy'
169-
};
187+
// Under the dashboard version header the endpoint URL comes back as target_url,
188+
// so read both before deciding there is nothing to test against.
189+
const url = this.selectedEndpoint?.url || this.selectedEndpoint?.target_url;
190+
if (!url) {
191+
this.generalService.showNotification({ message: 'Endpoint has no URL to test against', style: 'error' });
192+
return;
193+
}
194+
195+
const data = { data: 'test event from Convoy', convoy: 'https://getconvoy.io', amount: 1000 };
196+
197+
// Only templated endpoints (e.g. /tx/{reference}/callback) use the dynamic path:
198+
// it resolves the concrete URL against the endpoint template and bypasses
199+
// subscription event-type filters. Concrete endpoints keep the endpoint-bound
200+
// path so the test stays tied to the selected endpoint (its secrets, auth and
201+
// state); routing them through dynamic would bind by URL match and could
202+
// auto-create an orphan endpoint when the URL does not match exactly.
203+
//
204+
// The dynamic worker only runs template matching when both the license and the
205+
// org feature flag are on. If either is off it skips the lookup and mints a new
206+
// orphan endpoint for the URL, so we require both here and otherwise fall back
207+
// to the endpoint-bound path.
208+
const isTemplated = /\{[A-Za-z_][A-Za-z0-9_]*\}/.test(url);
170209

171210
this.isSendingTestEvent = true;
172211
try {
173-
const response = await this.endpointService.sendEvent({ body: testEvent });
212+
// Wait for the feature flag check kicked off in ngOnInit so a fast click does
213+
// not misroute a templated endpoint to the endpoint-bound path (which cannot
214+
// resolve the template) just because the check is still in flight.
215+
await this.featureFlagReady;
216+
const useDynamic = isTemplated && this.endpointURLTemplatesFeatureEnabled && this.licenseService.hasLicense('EndpointURLTemplates');
217+
218+
// For the templated path, substitute each {token} with a dummy value so the
219+
// URL is concrete; event_types (plural) is intentionally omitted so the
220+
// endpoint's real subscription filter is not overwritten.
221+
const response = useDynamic
222+
? await this.endpointService.sendDynamicEvent({
223+
body: { url: url.replace(/\{[A-Za-z_][A-Za-z0-9_]*\}/g, () => this.generateTestToken()), data, event_type: 'test.convoy' }
224+
})
225+
: await this.endpointService.sendEvent({ body: { data, endpoint_id: this.selectedEndpoint?.uid, event_type: 'test.convoy' } });
174226
this.generalService.showNotification({ message: response.message, style: 'success' });
175227
this.isSendingTestEvent = false;
176228
} catch {
177229
this.isSendingTestEvent = false;
178230
}
179231
}
180232

233+
private generateTestToken(): string {
234+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
235+
return crypto.randomUUID().replace(/-/g, '');
236+
}
237+
return `test${Date.now()}${Math.random().toString(36).slice(2)}`;
238+
}
239+
181240
viewSubscription() {
182241
this.router.navigate([`/projects/${this.privateService.getProjectDetails?.uid}/subscriptions`], { queryParams: { endpointId: this.selectedEndpoint?.uid || '' } });
183242
}

web/ui/dashboard/src/app/private/pages/project/endpoints/endpoints.service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ export class EndpointsService {
7474
});
7575
}
7676

77+
sendDynamicEvent(requestDetails: { body: any }): Promise<HTTP_RESPONSE> {
78+
return new Promise(async (resolve, reject) => {
79+
try {
80+
const response = await this.http.request({
81+
url: `/events/dynamic`,
82+
body: requestDetails.body,
83+
method: 'post',
84+
level: 'org_project'
85+
});
86+
87+
return resolve(response);
88+
} catch (error) {
89+
return reject(error);
90+
}
91+
});
92+
}
93+
7794
toggleEndpoint(endpointId: string): Promise<HTTP_RESPONSE> {
7895
return new Promise(async (resolve, reject) => {
7996
try {

web/ui/dashboard/src/app/private/pages/project/events/event-delivery-details/event-delivery-details.component.html

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ <h3 class="text-14 text-neutral-12 font-semibold mb-16px">Event Delivery Details
7474
<p class="text-neutral-10 text-12 font-light mb-6px">EVENT RECEIVED</p>
7575
@if (eventDelsDetails?.event_metadata?.created_at) {
7676
<convoy-copy-button [text]="formatPreciseTimestamp(eventDelsDetails?.event_metadata?.created_at)" show-icon="false" notificationText="Event received timestamp copied to clipboard">
77-
<span class="relative inline-flex text-12 font-medium group" [title]="formatPreciseTimestamp(eventDelsDetails?.event_metadata?.created_at)">
77+
<span class="relative inline-flex items-center text-12 font-medium group" [title]="formatPreciseTimestamp(eventDelsDetails?.event_metadata?.created_at)">
7878
<span>{{ formatLocalTimestamp(eventDelsDetails?.event_metadata?.created_at) }}</span>
79+
<svg class="ml-4px h-12px w-12px shrink-0 opacity-0 transition-opacity group-hover:opacity-100 stroke-neutral-10"><use xlink:href="#copy-icon-3"></use></svg>
7980
<span class="pointer-events-none absolute left-1/2 bottom-[calc(100%+8px)] z-50 -translate-x-1/2 whitespace-nowrap rounded-6px bg-neutral-12 px-8px py-4px text-10 text-white-100 opacity-0 shadow-md transition-opacity group-hover:opacity-100">
8081
{{ formatPreciseTimestamp(eventDelsDetails?.event_metadata?.created_at) }}
8182
</span>
@@ -92,8 +93,9 @@ <h3 class="text-14 text-neutral-12 font-semibold mb-16px">Event Delivery Details
9293
<p class="text-neutral-10 text-12 font-light mb-6px">ACKNOWLEDGED</p>
9394
@if (eventDelsDetails?.acknowledged_at) {
9495
<convoy-copy-button [text]="formatPreciseTimestamp(eventDelsDetails?.acknowledged_at)" show-icon="false" notificationText="Acknowledged timestamp copied to clipboard">
95-
<span class="relative inline-flex text-12 font-medium group" [title]="formatPreciseTimestamp(eventDelsDetails?.acknowledged_at)">
96+
<span class="relative inline-flex items-center text-12 font-medium group" [title]="formatPreciseTimestamp(eventDelsDetails?.acknowledged_at)">
9697
<span>{{ formatLocalTimestamp(eventDelsDetails?.acknowledged_at) }}</span>
98+
<svg class="ml-4px h-12px w-12px shrink-0 opacity-0 transition-opacity group-hover:opacity-100 stroke-neutral-10"><use xlink:href="#copy-icon-3"></use></svg>
9799
<span class="pointer-events-none absolute left-1/2 bottom-[calc(100%+8px)] z-50 -translate-x-1/2 whitespace-nowrap rounded-6px bg-neutral-12 px-8px py-4px text-10 text-white-100 opacity-0 shadow-md transition-opacity group-hover:opacity-100">
98100
{{ formatPreciseTimestamp(eventDelsDetails?.acknowledged_at) }}
99101
</span>
@@ -110,8 +112,9 @@ <h3 class="text-14 text-neutral-12 font-semibold mb-16px">Event Delivery Details
110112
<p class="text-neutral-10 text-12 font-light mb-6px">DELIVERY QUEUED</p>
111113
@if (eventDelsDetails?.created_at) {
112114
<convoy-copy-button [text]="formatPreciseTimestamp(eventDelsDetails?.created_at)" show-icon="false" notificationText="Delivery queued timestamp copied to clipboard">
113-
<span class="relative inline-flex text-12 font-medium group" [title]="formatPreciseTimestamp(eventDelsDetails?.created_at)">
115+
<span class="relative inline-flex items-center text-12 font-medium group" [title]="formatPreciseTimestamp(eventDelsDetails?.created_at)">
114116
<span>{{ formatLocalTimestamp(eventDelsDetails?.created_at) }}</span>
117+
<svg class="ml-4px h-12px w-12px shrink-0 opacity-0 transition-opacity group-hover:opacity-100 stroke-neutral-10"><use xlink:href="#copy-icon-3"></use></svg>
115118
<span class="pointer-events-none absolute left-1/2 bottom-[calc(100%+8px)] z-50 -translate-x-1/2 whitespace-nowrap rounded-6px bg-neutral-12 px-8px py-4px text-10 text-white-100 opacity-0 shadow-md transition-opacity group-hover:opacity-100">
116119
{{ formatPreciseTimestamp(eventDelsDetails?.created_at) }}
117120
</span>
@@ -128,8 +131,9 @@ <h3 class="text-14 text-neutral-12 font-semibold mb-16px">Event Delivery Details
128131
<p class="text-neutral-10 text-12 font-light mb-6px">LATEST ATTEMPT</p>
129132
@if (eventDeliveryAtempt?.created_at) {
130133
<convoy-copy-button [text]="formatPreciseTimestamp(eventDeliveryAtempt?.created_at)" show-icon="false" notificationText="Latest attempt timestamp copied to clipboard">
131-
<span class="relative inline-flex text-12 font-medium group" [title]="formatPreciseTimestamp(eventDeliveryAtempt?.created_at)">
134+
<span class="relative inline-flex items-center text-12 font-medium group" [title]="formatPreciseTimestamp(eventDeliveryAtempt?.created_at)">
132135
<span>{{ formatLocalTimestamp(eventDeliveryAtempt?.created_at) }}</span>
136+
<svg class="ml-4px h-12px w-12px shrink-0 opacity-0 transition-opacity group-hover:opacity-100 stroke-neutral-10"><use xlink:href="#copy-icon-3"></use></svg>
133137
<span class="pointer-events-none absolute left-1/2 bottom-[calc(100%+8px)] z-50 -translate-x-1/2 whitespace-nowrap rounded-6px bg-neutral-12 px-8px py-4px text-10 text-white-100 opacity-0 shadow-md transition-opacity group-hover:opacity-100">
134138
{{ formatPreciseTimestamp(eventDeliveryAtempt?.created_at) }}
135139
</span>
@@ -238,12 +242,22 @@ <h3 class="text-14 text-neutral-12 font-semibold mb-16px">Endpoint Details</h3>
238242
}
239243
</div>
240244

241-
<div class="text-12 text-neutral-12 truncate max-w-[370px]">{{ eventDelsDetails?.endpoint_metadata?.url || '-' }}</div>
245+
<convoy-copy-button [text]="eventDelsDetails?.target_url || eventDelsDetails?.endpoint_metadata?.url || ''" show-icon="false" notificationText="Endpoint URL copied to clipboard">
246+
<span class="relative inline-flex items-center max-w-[370px] group cursor-pointer">
247+
<span class="truncate text-12 text-neutral-12">{{ eventDelsDetails?.target_url || eventDelsDetails?.endpoint_metadata?.url || '-' }}</span>
248+
<svg class="ml-6px h-12px w-12px shrink-0 opacity-0 transition-opacity group-hover:opacity-100 stroke-neutral-10">
249+
<use xlink:href="#copy-icon-3"></use>
250+
</svg>
251+
<span class="pointer-events-none absolute left-0 bottom-[calc(100%+8px)] z-50 max-w-[560px] truncate whitespace-nowrap rounded-6px bg-neutral-12 px-8px py-4px text-10 text-white-100 opacity-0 shadow-md transition-opacity group-hover:opacity-100">
252+
{{ eventDelsDetails?.target_url || eventDelsDetails?.endpoint_metadata?.url }}
253+
</span>
254+
</span>
255+
</convoy-copy-button>
242256

243257
<div class="flex items-center gap-24px">
244258
<convoy-copy-button [text]="eventDelsDetails?.endpoint_metadata?.url || ''" color="primary" show-icon="false" [notificationText]="eventDelsDetails?.endpoint_metadata?.name + ' URL has been copied to clipboard'">
245259
<div convoy-tag color="primary" class="flex items-center !gap-12px">
246-
<span class="max-w-[230px] truncate">{{ eventDelsDetails?.endpoint_metadata?.url || '-' }}</span>
260+
<span class="max-w-[230px] overflow-x-auto whitespace-nowrap">@if (eventDelsDetails?.endpoint_metadata?.url) {@for (part of (eventDelsDetails?.endpoint_metadata?.url | urlTemplateParts); track $index) {<span [class]="part.token ? 'text-new.primary-400 font-medium bg-new.primary-25 rounded-[3px] px-[3px]' : ''">{{ part.text }}</span>}} @else {-}</span>
247261
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="scale-[.6]" xmlns="http://www.w3.org/2000/svg">
248262
<path d="M8 12.2002H15" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
249263
<path d="M8 16.2002H12.38" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />

web/ui/dashboard/src/app/private/pages/project/events/event-delivery-details/event-delivery-details.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { TagComponent } from 'src/app/components/tag/tag.component';
1010
import { SkeletonLoaderComponent } from 'src/app/components/skeleton-loader/skeleton-loader.component';
1111
import { StatusColorModule } from 'src/app/pipes/status-color/status-color.module';
1212
import { CopyButtonComponent } from 'src/app/components/copy-button/copy-button.component';
13+
import { UrlTemplatePartsPipe } from 'src/app/pipes/url-template-parts/url-template-parts.pipe';
1314
@NgModule({
1415
declarations: [EventDeliveryDetailsComponent],
15-
imports: [CommonModule, RouterModule, PrismModule, LoaderModule, CardComponent, ButtonComponent, TagComponent, StatusColorModule, SkeletonLoaderComponent, CopyButtonComponent],
16+
imports: [CommonModule, RouterModule, PrismModule, LoaderModule, CardComponent, ButtonComponent, TagComponent, StatusColorModule, SkeletonLoaderComponent, CopyButtonComponent, UrlTemplatePartsPipe],
1617
exports: [EventDeliveryDetailsComponent]
1718
})
1819
export class EventDeliveryDetailsModule {}

0 commit comments

Comments
 (0)