Skip to content

Commit aecd8a0

Browse files
authored
[NU-1828] Notification reloading creator panel when configuration rel… (#7387)
* [NU-1828] Notification reloading creator panel when configuration reloaded * fix for reload + notification also when reload failed * chagelog entry * silent configuration reloaded notification * review fixes * test fix
1 parent 3402166 commit aecd8a0

File tree

13 files changed

+266
-34
lines changed

13 files changed

+266
-34
lines changed

designer/client/src/containers/Notifications.tsx

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import Notification from "../components/notifications/Notification";
1010
import CheckCircleOutlinedIcon from "@mui/icons-material/CheckCircleOutlined";
1111
import DangerousOutlinedIcon from "@mui/icons-material/DangerousOutlined";
1212
import { markBackendNotificationRead, updateBackendNotifications } from "../actions/nk/notifications";
13-
import { loadProcessState } from "../actions/nk";
14-
import { getProcessName, getProcessVersionId } from "../reducers/selectors/graph";
13+
import { fetchProcessDefinition, loadProcessState } from "../actions/nk";
14+
import { getProcessingType, getProcessName, getProcessVersionId, isFragment } from "../reducers/selectors/graph";
1515
import { useChangeConnectionError } from "./connectionErrorProvider";
1616
import i18next from "i18next";
1717
import { ThunkAction } from "../actions/reduxTypes";
@@ -44,23 +44,37 @@ const prepareNotification =
4444
};
4545

4646
const handleRefresh =
47-
({ scenarioName, toRefresh }: BackendNotification, currentScenarioName: string, processVersionId: number): ThunkAction =>
47+
(
48+
{ scenarioName, toRefresh }: BackendNotification,
49+
currentScenarioName: string,
50+
processVersionId: number,
51+
currentProcessingType: string,
52+
currentIsFragment: boolean,
53+
): ThunkAction =>
4854
(dispatch) => {
49-
if (!scenarioName || scenarioName !== currentScenarioName) {
55+
if (scenarioName && scenarioName !== currentScenarioName) {
5056
return;
5157
}
52-
toRefresh.forEach((data) => {
53-
switch (data) {
54-
case "activity":
55-
return dispatch(getScenarioActivities(scenarioName));
56-
case "state":
57-
return dispatch(loadProcessState(scenarioName, processVersionId));
58-
}
59-
});
58+
if (toRefresh.indexOf("activity") >= 0 && currentScenarioName) {
59+
dispatch(getScenarioActivities(currentScenarioName));
60+
}
61+
if (toRefresh.indexOf("state") >= 0 && currentScenarioName && processVersionId) {
62+
dispatch(loadProcessState(currentScenarioName, processVersionId));
63+
}
64+
if (toRefresh.indexOf("creator") >= 0 && currentProcessingType && currentIsFragment != null) {
65+
dispatch(fetchProcessDefinition(currentProcessingType, currentIsFragment));
66+
}
67+
return;
6068
};
6169

6270
const prepareNotifications =
63-
(notifications: BackendNotification[], scenarioName: string, processVersionId: number): ThunkAction =>
71+
(
72+
notifications: BackendNotification[],
73+
scenarioName: string,
74+
processVersionId: number,
75+
currentProcessingType: string,
76+
currentIsFragment: boolean,
77+
): ThunkAction =>
6478
(dispatch, getState) => {
6579
const state = getState();
6680
const { processedNotificationIds } = getBackendNotifications(state);
@@ -74,7 +88,7 @@ const prepareNotifications =
7488

7589
notifications.filter(onlyUnreadPredicate).forEach((notification) => {
7690
dispatch(prepareNotification(notification));
77-
dispatch(handleRefresh(notification, scenarioName, processVersionId));
91+
dispatch(handleRefresh(notification, scenarioName, processVersionId, currentProcessingType, currentIsFragment));
7892
});
7993
};
8094

@@ -87,13 +101,17 @@ export function Notifications(): JSX.Element {
87101

88102
const currentScenarioName = useSelector(getProcessName);
89103
const processVersionId = useSelector(getProcessVersionId);
104+
const currentProcessingType = useSelector(getProcessingType);
105+
const currentIsFragment = useSelector(isFragment);
90106

91107
const refresh = useCallback(() => {
92108
HttpService.loadBackendNotifications(currentScenarioName)
93109
.then((notifications) => {
94110
handleChangeConnectionError(null);
95111
dispatch(updateBackendNotifications(notifications.map(({ id }) => id)));
96-
dispatch(prepareNotifications(notifications, currentScenarioName, processVersionId));
112+
dispatch(
113+
prepareNotifications(notifications, currentScenarioName, processVersionId, currentProcessingType, currentIsFragment),
114+
);
97115
})
98116
.catch((error) => {
99117
const isNetworkAccess = navigator.onLine;
@@ -111,7 +129,7 @@ export function Notifications(): JSX.Element {
111129
);
112130
}
113131
});
114-
}, [currentScenarioName, dispatch, handleChangeConnectionError]);
132+
}, [currentScenarioName, processVersionId, currentProcessingType, currentIsFragment, dispatch, handleChangeConnectionError]);
115133
useInterval(refresh, {
116134
refreshTime: 2000,
117135
ignoreFirst: true,
@@ -123,7 +141,7 @@ export function Notifications(): JSX.Element {
123141

124142
type NotificationType = "info" | "error" | "success";
125143

126-
type DataToRefresh = "activity" | "state";
144+
type DataToRefresh = "activity" | "state" | "creator";
127145

128146
export type BackendNotification = {
129147
id: string;

designer/server/src/main/scala/pl/touk/nussknacker/ui/notifications/Notification.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import pl.touk.nussknacker.ui.util.ScenarioActivityUtils.ScenarioActivityOps
1515
import sttp.tapir.Schema
1616
import sttp.tapir.derevo.schema
1717

18+
import java.util.UUID
19+
1820
@derive(encoder, decoder, schema)
1921
final case class Notification(
2022
id: String,
@@ -92,6 +94,16 @@ object Notification {
9294
)
9395
}
9496

97+
def configurationReloaded: Notification =
98+
Notification(
99+
id = UUID.randomUUID().toString,
100+
scenarioName = None,
101+
message = "Configuration reloaded",
102+
// We don't want to show this notification to other users because they might be not interested, and it can only introduce a confusion
103+
`type` = None,
104+
toRefresh = List(DataToRefresh.creator)
105+
)
106+
95107
private def displayableActionName(actionName: ScenarioActionName): String =
96108
actionName match {
97109
case ScenarioActionName.Deploy => "Deployment"
@@ -116,5 +128,5 @@ object DataToRefresh extends Enumeration {
116128
implicit val typeDecoder: Decoder[DataToRefresh.Value] = Decoder.decodeEnumeration(DataToRefresh)
117129

118130
type DataToRefresh = Value
119-
val activity, state = Value
131+
val activity, state, creator = Value
120132
}

designer/server/src/main/scala/pl/touk/nussknacker/ui/notifications/NotificationService.scala

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import pl.touk.nussknacker.ui.notifications.NotificationService.NotificationsSco
66
import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, ScenarioActionRepository}
77
import pl.touk.nussknacker.ui.process.scenarioactivity.FetchScenarioActivityService
88
import pl.touk.nussknacker.ui.security.api.LoggedUser
9+
import pl.touk.nussknacker.ui.util.InMemoryTimeseriesRepository
910

1011
import java.time.{Clock, Instant}
1112
import scala.concurrent.duration.FiniteDuration
@@ -43,6 +44,7 @@ object NotificationService {
4344
class NotificationServiceImpl(
4445
fetchScenarioActivityService: FetchScenarioActivityService,
4546
scenarioActionRepository: ScenarioActionRepository,
47+
globalNotificationRepository: InMemoryTimeseriesRepository[Notification],
4648
dbioRunner: DBIOActionRunner,
4749
config: NotificationConfig,
4850
clock: Clock = Clock.systemUTC()
@@ -53,14 +55,19 @@ class NotificationServiceImpl(
5355
)(implicit ec: ExecutionContext): Future[List[Notification]] = {
5456
val now = clock.instant()
5557
val limit = now.minusMillis(config.duration.toMillis)
58+
def fetchUserAndGlobalNotifications(user: LoggedUser) =
59+
for {
60+
notificationsForUserActions <- notificationsForUserActions(user, limit)
61+
globalNotifications = fetchGlobalNotificationsAndTriggerEviction(limit)
62+
} yield notificationsForUserActions ++ globalNotifications
5663
scope match {
5764
case NotificationsScope.NotificationsForLoggedUser(user) =>
58-
notificationsForUserActions(user, limit)
65+
fetchUserAndGlobalNotifications(user)
5966
case NotificationsScope.NotificationsForLoggedUserAndScenario(user, processName) =>
6067
for {
61-
notificationsForUserActions <- notificationsForUserActions(user, limit)
68+
userAndGlobalNotifications <- fetchUserAndGlobalNotifications(user)
6269
notificationsForScenarioActivities <- notificationsForScenarioActivities(user, processName, limit)
63-
} yield notificationsForUserActions ++ notificationsForScenarioActivities
70+
} yield userAndGlobalNotifications ++ notificationsForScenarioActivities
6471
}
6572

6673
}
@@ -107,4 +114,10 @@ class NotificationServiceImpl(
107114
} yield notificationsForScenarioActivities
108115
}
109116

117+
private def fetchGlobalNotificationsAndTriggerEviction(limit: Instant) = {
118+
val globalNotifications = globalNotificationRepository.fetchEntries(limit)
119+
globalNotificationRepository.evictOldEntries()
120+
globalNotifications
121+
}
122+
110123
}

designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import pl.touk.nussknacker.ui.config.scenariotoolbar.CategoriesScenarioToolbarsC
2525
import pl.touk.nussknacker.ui.config.{
2626
AttachmentsConfig,
2727
ComponentLinksConfigExtractor,
28+
DesignerConfig,
2829
FeatureTogglesConfig,
2930
UsageStatisticsReportsConfig
3031
}
31-
import pl.touk.nussknacker.ui.config.DesignerConfig
3232
import pl.touk.nussknacker.ui.db.DbRef
3333
import pl.touk.nussknacker.ui.db.timeseries.FEStatisticsRepository
3434
import pl.touk.nussknacker.ui.definition.component.{ComponentServiceProcessingTypeData, DefaultComponentService}
@@ -43,7 +43,7 @@ import pl.touk.nussknacker.ui.listener.ProcessChangeListenerLoader
4343
import pl.touk.nussknacker.ui.listener.services.NussknackerServices
4444
import pl.touk.nussknacker.ui.metrics.RepositoryGauges
4545
import pl.touk.nussknacker.ui.migrations.{MigrationApiAdapterService, MigrationService}
46-
import pl.touk.nussknacker.ui.notifications.{NotificationConfig, NotificationServiceImpl}
46+
import pl.touk.nussknacker.ui.notifications.{Notification, NotificationConfig, NotificationServiceImpl}
4747
import pl.touk.nussknacker.ui.process._
4848
import pl.touk.nussknacker.ui.process.deployment.{
4949
ActionService,
@@ -94,7 +94,7 @@ import pl.touk.nussknacker.ui.validation.{
9494
}
9595
import sttp.client3.SttpBackend
9696

97-
import java.time.Clock
97+
import java.time.{Clock, Duration}
9898
import java.util.concurrent.atomic.AtomicReference
9999
import java.util.function.Supplier
100100
import scala.concurrent.Future
@@ -130,13 +130,16 @@ class AkkaHttpBasedRouteProvider(
130130
deploymentRepository = new DeploymentRepository(dbRef, Clock.systemDefaultZone())
131131
scenarioActivityRepository = DbScenarioActivityRepository.create(dbRef, designerClock)
132132
dbioRunner = DBIOActionRunner(dbRef)
133+
// 1 hour is the delay to propagate all global notifications for all users
134+
globalNotificationRepository = InMemoryTimeseriesRepository[Notification](Duration.ofHours(1), Clock.systemUTC())
133135
processingTypeDataProvider <- prepareProcessingTypeDataReload(
134136
additionalUIConfigProvider,
135137
actionServiceSupplier,
136138
scenarioActivityRepository,
137139
dbioRunner,
138140
sttpBackend,
139141
featureTogglesConfig,
142+
globalNotificationRepository
140143
)
141144

142145
deploymentsStatusesSynchronizer = new DeploymentsStatusesSynchronizer(
@@ -325,6 +328,7 @@ class AkkaHttpBasedRouteProvider(
325328
val notificationService = new NotificationServiceImpl(
326329
fetchScenarioActivityService,
327330
actionRepository,
331+
globalNotificationRepository,
328332
dbioRunner,
329333
notificationsConfig
330334
)
@@ -699,7 +703,8 @@ class AkkaHttpBasedRouteProvider(
699703
scenarioActivityRepository: ScenarioActivityRepository,
700704
dbioActionRunner: DBIOActionRunner,
701705
sttpBackend: SttpBackend[Future, Any],
702-
featureTogglesConfig: FeatureTogglesConfig
706+
featureTogglesConfig: FeatureTogglesConfig,
707+
globalNotificationRepository: InMemoryTimeseriesRepository[Notification]
703708
): Resource[IO, ReloadableProcessingTypeDataProvider] = {
704709
Resource
705710
.make(
@@ -719,7 +724,12 @@ class AkkaHttpBasedRouteProvider(
719724
_
720725
),
721726
)
722-
new ReloadableProcessingTypeDataProvider(laodProcessingTypeDataIO)
727+
val loadAndNotifyIO = laodProcessingTypeDataIO
728+
.map { state =>
729+
globalNotificationRepository.saveEntry(Notification.configurationReloaded)
730+
state
731+
}
732+
new ReloadableProcessingTypeDataProvider(loadAndNotifyIO)
723733
}
724734
)(
725735
release = _.close()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package pl.touk.nussknacker.ui.util
2+
3+
import java.time.{Clock, Duration, Instant}
4+
import java.util.concurrent.ConcurrentSkipListMap
5+
import scala.jdk.CollectionConverters._
6+
7+
class InMemoryTimeseriesRepository[EntryT](
8+
timeline: ConcurrentSkipListMap[Instant, EntryT],
9+
evictionDelay: Duration,
10+
clock: Clock
11+
) {
12+
13+
def saveEntry(entry: EntryT): Unit = {
14+
timeline.put(clock.instant(), entry)
15+
}
16+
17+
def fetchEntries(lowerLimit: Instant): List[EntryT] = {
18+
timeline.tailMap(lowerLimit).values().asScala.toList
19+
}
20+
21+
def evictOldEntries(): Unit = {
22+
timeline.headMap(clock.instant().minus(evictionDelay), true).clear()
23+
}
24+
25+
}
26+
27+
object InMemoryTimeseriesRepository {
28+
29+
def apply[EntryT](retentionDelay: Duration, clock: Clock): InMemoryTimeseriesRepository[EntryT] = {
30+
new InMemoryTimeseriesRepository[EntryT](
31+
new ConcurrentSkipListMap[Instant, EntryT],
32+
retentionDelay,
33+
clock
34+
)
35+
}
36+
37+
}

designer/server/src/test/scala/pl/touk/nussknacker/ui/api/NotificationApiHttpServiceBusinessSpec.scala

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,53 @@ class NotificationApiHttpServiceBusinessSpec
2525
with PatientScalaFutures {
2626

2727
"The endpoint for getting notifications should" - {
28-
"return empty list if no notifications are present" in {
28+
// We can't easily recognize if configuration was changed between restarts so just in case we send this notification
29+
"return initial notification about configuration reload just after start of application" in {
2930
given()
3031
.when()
3132
.basicAuthAdmin()
3233
.get(s"$nuDesignerHttpAddress/api/notifications")
3334
.Then()
3435
.statusCode(200)
3536
.body(
36-
equalTo("[]")
37+
matchJsonWithRegexValues(
38+
s"""[{
39+
| "id": "^\\\\w{8}-\\\\w{4}-\\\\w{4}-\\\\w{4}-\\\\w{12}$$",
40+
| "scenarioName": null,
41+
| "message": "Configuration reloaded",
42+
| "type": null,
43+
| "toRefresh": [ "creator" ]
44+
|}]""".stripMargin
45+
)
46+
)
47+
}
48+
"return notification when processing type data are reloaded" in {
49+
given()
50+
.when()
51+
.applicationState {
52+
reloadConfiguration()
53+
}
54+
.basicAuthAdmin()
55+
.get(s"$nuDesignerHttpAddress/api/notifications")
56+
.Then()
57+
.statusCode(200)
58+
.body(
59+
matchJsonWithRegexValues(
60+
s"""[{
61+
| "id": "^\\\\w{8}-\\\\w{4}-\\\\w{4}-\\\\w{4}-\\\\w{12}$$",
62+
| "scenarioName": null,
63+
| "message": "Configuration reloaded",
64+
| "type": null,
65+
| "toRefresh": [ "creator" ]
66+
|},
67+
|{
68+
| "id": "^\\\\w{8}-\\\\w{4}-\\\\w{4}-\\\\w{4}-\\\\w{12}$$",
69+
| "scenarioName": null,
70+
| "message": "Configuration reloaded",
71+
| "type": null,
72+
| "toRefresh": [ "creator" ]
73+
|}]""".stripMargin
74+
)
3775
)
3876
}
3977
"return a list of notifications" in {
@@ -62,6 +100,20 @@ class NotificationApiHttpServiceBusinessSpec
62100
| "message": "Cancel finished",
63101
| "type": null,
64102
| "toRefresh": [ "activity", "state" ]
103+
|},
104+
|{
105+
| "id": "^\\\\w{8}-\\\\w{4}-\\\\w{4}-\\\\w{4}-\\\\w{12}$$",
106+
| "scenarioName": null,
107+
| "message": "Configuration reloaded",
108+
| "type": null,
109+
| "toRefresh": [ "creator" ]
110+
|},
111+
|{
112+
| "id": "^\\\\w{8}-\\\\w{4}-\\\\w{4}-\\\\w{4}-\\\\w{12}$$",
113+
| "scenarioName": null,
114+
| "message": "Configuration reloaded",
115+
| "type": null,
116+
| "toRefresh": [ "creator" ]
65117
|}]""".stripMargin
66118
)
67119
)
@@ -81,4 +133,13 @@ class NotificationApiHttpServiceBusinessSpec
81133
}
82134
}
83135

136+
private def reloadConfiguration(): Unit = {
137+
given()
138+
.when()
139+
.basicAuthAdmin()
140+
.post(s"$nuDesignerHttpAddress/api/app/processingtype/reload")
141+
.Then()
142+
.statusCode(204)
143+
}
144+
84145
}

0 commit comments

Comments
 (0)