Skip to content

Commit 979138f

Browse files
authored
Feature/651 show notification usage (#657)
#651 show notification usage (#657)
1 parent f4fc29f commit 979138f

22 files changed

+565
-16
lines changed

src/main/scala/za/co/absa/hyperdrive/trigger/api/rest/controllers/NotificationRuleController.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ class NotificationRuleController @Inject()(notificationRuleService: Notification
5959
def searchNotificationRules(@RequestBody searchRequest: TableSearchRequest): CompletableFuture[TableSearchResponse[NotificationRule]] = {
6060
notificationRuleService.searchNotificationRules(searchRequest).toJava.toCompletableFuture
6161
}
62+
63+
@GetMapping(path = Array("/notificationRules/{id}/workflows"))
64+
def getMatchingWorkflows(@PathVariable id: Long): CompletableFuture[Seq[Workflow]] = {
65+
notificationRuleService.getMatchingWorkflows(id).toJava.toCompletableFuture
66+
}
6267
}

src/main/scala/za/co/absa/hyperdrive/trigger/api/rest/services/NotificationRuleService.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ trait NotificationRuleService {
4242

4343
def getMatchingNotificationRules(workflowId: Long, status: DagInstanceStatus)(implicit ec: ExecutionContext): Future[Option[(Seq[NotificationRule], Workflow)]]
4444

45+
def getMatchingWorkflows(id: Long)(implicit ec: ExecutionContext): Future[Seq[Workflow]]
4546
}
4647

4748
@Service
@@ -84,4 +85,8 @@ class NotificationRuleServiceImpl(override val notificationRuleRepository: Notif
8485
override def getMatchingNotificationRules(workflowId: Long, status: DagInstanceStatus)(implicit ec: ExecutionContext): Future[Option[(Seq[NotificationRule], Workflow)]] = {
8586
notificationRuleRepository.getMatchingNotificationRules(workflowId, status, LocalDateTime.now())
8687
}
88+
89+
override def getMatchingWorkflows(id: Long)(implicit ec: ExecutionContext): Future[Seq[Workflow]] = {
90+
notificationRuleRepository.getMatchingWorkflows(id)
91+
}
8792
}

src/main/scala/za/co/absa/hyperdrive/trigger/persistance/NotificationRuleRepository.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ trait NotificationRuleRepository extends Repository {
4545

4646
def getMatchingNotificationRules(workflowId: Long, status: DagInstanceStatus, currentTime: LocalDateTime)(implicit ec: ExecutionContext): Future[Option[(Seq[NotificationRule], Workflow)]]
4747

48+
def getMatchingWorkflows(id: Long)(implicit ec: ExecutionContext): Future[Seq[Workflow]]
49+
4850
}
4951

5052
@stereotype.Repository
@@ -136,6 +138,24 @@ class NotificationRuleRepositoryImpl @Inject()(
136138
)
137139
}
138140

141+
override def getMatchingWorkflows(id: Long)(implicit ec: ExecutionContext): Future[Seq[Workflow]] = {
142+
db.run(
143+
getNotificationRuleInternal(id).flatMap { notificationRule =>
144+
workflowTable
145+
.filter { workflow =>
146+
val workflowPrefix = notificationRule.workflowPrefix.getOrElse("")
147+
LiteralColumn[Boolean](workflowPrefix.isEmpty) ||
148+
workflow.name.toLowerCase.like(workflowPrefix.toLowerCase ++ "%")
149+
}
150+
.filter { workflow =>
151+
val project = notificationRule.project.getOrElse("")
152+
LiteralColumn[Boolean](project.isEmpty) ||
153+
workflow.project.toLowerCase === project.toLowerCase
154+
}.result
155+
}
156+
)
157+
}
158+
139159
private def insertNotificationRuleInternal(notificationRule: NotificationRule, user: String)(implicit ec: ExecutionContext) = {
140160
val notificationRuleToInsert = notificationRule.copy(created = LocalDateTime.now())
141161
for {

src/test/scala/za/co/absa/hyperdrive/trigger/api/rest/services/NotificationRuleServiceTest.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,16 @@ class NotificationRuleServiceTest extends AsyncFlatSpec with Matchers with Mocki
178178
result.value shouldBe matchingRules
179179
}
180180

181+
"getMatchingWorkflows" should "return matching workflows" in {
182+
// given
183+
val notificationRuleId: Long = 1
184+
val workflows = Seq(Workflow("workflow1", isActive = true, "project1", LocalDateTime.now(), None, 1, None, 42L))
185+
when(notificationRuleRepository.getMatchingWorkflows(eqTo(notificationRuleId))(any[ExecutionContext])).thenReturn(Future(workflows))
186+
187+
// when
188+
val result = await(underTest.getMatchingWorkflows(notificationRuleId))
189+
190+
// then
191+
result shouldBe workflows
192+
}
181193
}

src/test/scala/za/co/absa/hyperdrive/trigger/persistance/NotificationRuleRepositoryPostgresTest.scala

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,67 @@ class NotificationRuleRepositoryPostgresTest extends FlatSpec with Matchers with
162162
result.value._2 shouldBe w1
163163
}
164164

165+
"getMatchingWorkflows" should "return matching workflows" in {
166+
val w1 = Workflow(name = "workflow1", project = "project1", isActive = true, created = LocalDateTime.now(), updated = None, version = 1, id = 0)
167+
val w2 = Workflow(name = "workflow2", project = "project1", isActive = true, created = LocalDateTime.now(), updated = None, version = 1, id = 1)
168+
val w3 = Workflow(name = "workflow3", project = "project2", isActive = true, created = LocalDateTime.now(), updated = None, version = 1, id = 2)
169+
val w4 = Workflow(name = "workflow14", project = "project3", isActive = true, created = LocalDateTime.now(), updated = None, version = 1, id = 3)
170+
171+
val nr = createDummyNotificationRule()
172+
val nr1 = nr.copy(workflowPrefix = None, project = Some("project2"), id = 1)
173+
val nr2 = nr.copy(workflowPrefix = None, project = Some("PROJECT1"), id = 2)
174+
val nr3 = nr.copy(workflowPrefix = None, project = Some(""), id = 3)
175+
val nr4 = nr.copy(workflowPrefix = None, project = None, id = 4)
176+
val nr5 = nr.copy(workflowPrefix = None, project = Some("project"), id = 5)
177+
178+
val nr6 = nr.copy(workflowPrefix = Some("WORKFLOW"), project = None, id = 6)
179+
val nr7 = nr.copy(workflowPrefix = Some("WoRkFlOw1"), project = None, id = 7)
180+
val nr8 = nr.copy(workflowPrefix = Some(""), project = None, id = 8)
181+
val nr9 = nr.copy(workflowPrefix = None, project = None, id = 9)
182+
val nr10 = nr.copy(workflowPrefix = Some("xyz"), project = None, id = 10)
183+
184+
val nr11 = nr.copy(workflowPrefix = Some("wor"), project = Some("PROJECT1"), id = 11)
185+
186+
await(db.run(workflowTable.forceInsertAll(Seq(w1, w2, w3, w4))))
187+
await(db.run(notificationRuleTable.forceInsertAll(Seq(nr1, nr2, nr3, nr4, nr5, nr6, nr7, nr8, nr9, nr10, nr11))))
188+
189+
//Match on project
190+
val result1 = await(notificationRuleRepository.getMatchingWorkflows(nr1.id))
191+
result1 should contain theSameElementsAs Seq(w3)
192+
//Match on project only, ignoring case
193+
val result2 = await(notificationRuleRepository.getMatchingWorkflows(nr2.id))
194+
result2 should contain theSameElementsAs Seq(w1, w2)
195+
//Match on project only - empty string
196+
val result3 = await(notificationRuleRepository.getMatchingWorkflows(nr3.id))
197+
result3 should contain theSameElementsAs Seq(w1, w2, w3, w4)
198+
//Match on project only - none
199+
val result4 = await(notificationRuleRepository.getMatchingWorkflows(nr4.id))
200+
result4 should contain theSameElementsAs Seq(w1, w2, w3, w4)
201+
//Match on project only - no matching workflow
202+
val result5 = await(notificationRuleRepository.getMatchingWorkflows(nr5.id))
203+
result5 should contain theSameElementsAs Seq()
204+
205+
//Match on workflow prefix only, ignoring case - match all
206+
val result6 = await(notificationRuleRepository.getMatchingWorkflows(nr6.id))
207+
result6 should contain theSameElementsAs Seq(w1, w2, w3, w4)
208+
//Match on workflow prefix only - partial match
209+
val result7 = await(notificationRuleRepository.getMatchingWorkflows(nr7.id))
210+
result7 should contain theSameElementsAs Seq(w1, w4)
211+
//Match on workflow prefix only - empty string
212+
val result8 = await(notificationRuleRepository.getMatchingWorkflows(nr8.id))
213+
result8 should contain theSameElementsAs Seq(w1, w2, w3, w4)
214+
//Match on workflow prefix only - none
215+
val result9 = await(notificationRuleRepository.getMatchingWorkflows(nr9.id))
216+
result9 should contain theSameElementsAs Seq(w1, w2, w3, w4)
217+
//Match on workflow prefix only - no matching workflow
218+
val result10 = await(notificationRuleRepository.getMatchingWorkflows(nr10.id))
219+
result10 should contain theSameElementsAs Seq()
220+
221+
//Match on workflow prefix and project
222+
val result11 = await(notificationRuleRepository.getMatchingWorkflows(nr11.id))
223+
result11 should contain theSameElementsAs Seq(w1, w2)
224+
}
225+
165226
private def createDummyNotificationRule() = {
166227
NotificationRule(isActive = true, None, None, None, Seq(DagInstanceStatuses.Failed), Seq("[email protected]"),
167228
created = LocalDateTime.now(), updated = None, id = -1L)

ui/src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ import { JobTemplateHistoryComponent } from './components/admin/job-templates/jo
101101
import { JobTemplateComparisonComponent } from './components/admin/job-templates/job-template-history/job-template-comparison/job-template-comparison.component';
102102
import { JobTemplateUsageComponent } from './components/admin/job-templates/job-templates-home/job-template-usage/job-template-usage.component';
103103
import { LengthValidator } from './components/workflows/workflow-form/dynamic-parts/string-sequence-part/validator/length.validator';
104+
import { NotificationRuleUsageComponent } from './components/admin/notification-rules/notification-rules-home/notification-rule-usage/notification-rule-usage.component';
104105

105106
@NgModule({
106107
declarations: [
@@ -160,6 +161,7 @@ import { LengthValidator } from './components/workflows/workflow-form/dynamic-pa
160161
NotificationRuleComponent,
161162
NotificationRulesComponent,
162163
NotificationRulesHomeComponent,
164+
NotificationRuleUsageComponent,
163165
NotificationRulesFormComponent,
164166
NotificationRuleComparisonComponent,
165167
NotificationRuleHistoryComponent,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!--
2+
~ Copyright 2018 ABSA Group Limited
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~ http://www.apache.org/licenses/LICENSE-2.0
8+
~
9+
~ Unless required by applicable law or agreed to in writing, software
10+
~ distributed under the License is distributed on an "AS IS" BASIS,
11+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
~ See the License for the specific language governing permissions and
13+
~ limitations under the License.
14+
-->
15+
16+
<clr-datagrid [clrDgLoading]="loading">
17+
<clr-dg-column>Workflow Name</clr-dg-column>
18+
<clr-dg-column>Project Name</clr-dg-column>
19+
<clr-dg-column>Is Active</clr-dg-column>
20+
21+
<clr-dg-placeholder>Notification rule has not matched any workflow!</clr-dg-placeholder>
22+
23+
<clr-dg-row *ngFor="let workflow of workflows">
24+
<clr-dg-action-overflow>
25+
<button type="button" class="action-item" (click)="showWorkflow(workflow.id)">
26+
<clr-icon shape="eye"></clr-icon>
27+
Show
28+
</button>
29+
</clr-dg-action-overflow>
30+
<clr-dg-cell (dblclick)="showWorkflow(workflow.id)">{{workflow.name}}</clr-dg-cell>
31+
<clr-dg-cell (dblclick)="showWorkflow(workflow.id)">{{workflow.project}}</clr-dg-cell>
32+
<clr-dg-cell (dblclick)="showWorkflow(workflow.id)" [ngSwitch]="workflow.isActive">
33+
<clr-icon *ngSwitchCase="true" shape="circle" class="is-solid" style="color: green"></clr-icon>
34+
<clr-icon *ngSwitchCase="false" shape="circle" class="is-solid" style="color: red"></clr-icon>
35+
{{workflow.isActive ? 'Yes' : 'No'}}
36+
</clr-dg-cell>
37+
</clr-dg-row>
38+
</clr-datagrid>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*!
2+
* Copyright 2018 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2018 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
17+
18+
import { provideMockStore } from '@ngrx/store/testing';
19+
import { HttpClientTestingModule } from '@angular/common/http/testing';
20+
import { Store } from '@ngrx/store';
21+
import { Subject } from 'rxjs';
22+
import { AppState } from '../../../../../stores/app.reducers';
23+
import { Router } from '@angular/router';
24+
import { RouterTestingModule } from '@angular/router/testing';
25+
import { absoluteRoutes } from '../../../../../constants/routes.constants';
26+
import { NotificationRuleUsageComponent } from './notification-rule-usage.component';
27+
import { GetNotificationRuleUsage } from '../../../../../stores/notification-rules/notification-rules.actions';
28+
29+
describe('NotificationRuleUsageComponent', () => {
30+
let underTest: NotificationRuleUsageComponent;
31+
let fixture: ComponentFixture<NotificationRuleUsageComponent>;
32+
let store: Store<AppState>;
33+
let router: Router;
34+
35+
const initialAppState = {
36+
notificationRules: {
37+
usage: {
38+
loading: true,
39+
workflows: [],
40+
},
41+
},
42+
};
43+
44+
beforeEach(
45+
waitForAsync(() => {
46+
TestBed.configureTestingModule({
47+
providers: [provideMockStore({ initialState: initialAppState })],
48+
declarations: [NotificationRuleUsageComponent],
49+
imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])],
50+
}).compileComponents();
51+
store = TestBed.inject(Store);
52+
router = TestBed.inject(Router);
53+
}),
54+
);
55+
56+
beforeEach(() => {
57+
fixture = TestBed.createComponent(NotificationRuleUsageComponent);
58+
underTest = fixture.componentInstance;
59+
});
60+
61+
it('should create', () => {
62+
expect(underTest).toBeTruthy();
63+
});
64+
65+
it(
66+
'onInit should set component properties',
67+
waitForAsync(() => {
68+
fixture.detectChanges();
69+
fixture.whenStable().then(() => {
70+
expect(underTest.loading).toBe(initialAppState.notificationRules.usage.loading);
71+
expect(underTest.workflows).toBe(initialAppState.notificationRules.usage.workflows);
72+
});
73+
}),
74+
);
75+
76+
it(
77+
'onRefresh() should dispatch GetNotificationRuleUsage',
78+
waitForAsync(() => {
79+
underTest.notificationId = 42;
80+
underTest.refreshSubject = new Subject<boolean>();
81+
const subject = new Subject<boolean>();
82+
const storeSpy = spyOn(store, 'dispatch');
83+
84+
underTest.onRefresh();
85+
subject.next(true);
86+
87+
fixture.detectChanges();
88+
fixture.whenStable().then(() => {
89+
expect(storeSpy).toHaveBeenCalled();
90+
expect(storeSpy).toHaveBeenCalledWith(new GetNotificationRuleUsage(underTest.notificationId));
91+
});
92+
}),
93+
);
94+
95+
it(
96+
'showWorkflow() should navigate to show workflow page',
97+
waitForAsync(() => {
98+
const id = 42;
99+
const routerSpy = spyOn(router, 'navigate');
100+
101+
underTest.showWorkflow(id);
102+
103+
expect(routerSpy).toHaveBeenCalledTimes(1);
104+
expect(routerSpy).toHaveBeenCalledWith([absoluteRoutes.SHOW_WORKFLOW, id]);
105+
}),
106+
);
107+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2018 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
17+
import { Store } from '@ngrx/store';
18+
import { Subject, Subscription } from 'rxjs';
19+
import { WorkflowModel } from '../../../../../models/workflow.model';
20+
import { AppState, selectNotificationRulesState } from '../../../../../stores/app.reducers';
21+
import { Router } from '@angular/router';
22+
import { absoluteRoutes } from '../../../../../constants/routes.constants';
23+
import { GetNotificationRuleUsage } from '../../../../../stores/notification-rules/notification-rules.actions';
24+
25+
@Component({
26+
selector: 'app-notification-rule-usage',
27+
templateUrl: './notification-rule-usage.component.html',
28+
styleUrls: ['./notification-rule-usage.component.scss'],
29+
})
30+
export class NotificationRuleUsageComponent implements OnInit, OnDestroy {
31+
@Input('notificationId') notificationId: number;
32+
@Input() refreshSubject: Subject<boolean> = new Subject<boolean>();
33+
workflows: WorkflowModel[];
34+
loading = true;
35+
36+
notificationsSubscription: Subscription;
37+
refreshSubscription: Subscription;
38+
39+
constructor(private store: Store<AppState>, private router: Router) {}
40+
41+
ngOnInit() {
42+
this.store.dispatch(new GetNotificationRuleUsage(this.notificationId));
43+
this.notificationsSubscription = this.store.select(selectNotificationRulesState).subscribe((state) => {
44+
this.loading = state.usage.loading;
45+
this.workflows = state.usage.workflows;
46+
});
47+
48+
this.refreshSubscription = this.refreshSubject.subscribe((response) => {
49+
if (response) {
50+
this.onRefresh();
51+
}
52+
});
53+
}
54+
55+
onRefresh() {
56+
this.store.dispatch(new GetNotificationRuleUsage(this.notificationId));
57+
}
58+
59+
showWorkflow(id: number) {
60+
this.router.navigate([absoluteRoutes.SHOW_WORKFLOW, id]);
61+
}
62+
63+
ngOnDestroy(): void {
64+
!!this.notificationsSubscription && this.notificationsSubscription.unsubscribe();
65+
!!this.refreshSubscription && this.refreshSubscription.unsubscribe();
66+
}
67+
}

0 commit comments

Comments
 (0)