Skip to content

Commit f723d42

Browse files
KochTobiSteffengreinerShraddha0903
authored
Introduce announcement banner for maintenance notification (#1143)
* Show announcements * Fix code smells * Provide CSS for announcements * Update bundle * Use relational units for grid rows within project-overview * Adjust styling Co-authored-by: Shraddha Pawar <[email protected]> --------- Co-authored-by: KochTobi <[email protected]> Co-authored-by: Steffengreiner <[email protected]> Co-authored-by: Shraddha Pawar <[email protected]>
1 parent 1a64035 commit f723d42

File tree

16 files changed

+275
-33
lines changed

16 files changed

+275
-33
lines changed

sql/complete-schema.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,17 @@ CREATE TABLE IF NOT EXISTS `confounding_variable_levels`
577577
DEFAULT CHARSET = utf8mb4
578578
COLLATE utf8mb4_unicode_ci;
579579

580+
CREATE TABLE IF NOT EXISTS `announcements`
581+
(
582+
`id` bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
583+
`start_time` datetime(6) NOT NULL,
584+
`end_time` datetime(6) NOT NULL,
585+
`message` VARCHAR(255) NOT NULL
586+
) ENGINE = InnoDB
587+
AUTO_INCREMENT = 1
588+
DEFAULT CHARSET = utf8mb4
589+
COLLATE utf8mb4_unicode_ci;
590+
580591

581592

582593
DROP VIEW IF EXISTS data_management.project_measurements;

user-interface/frontend/themes/datamanager/components/all.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,27 @@ Layout
475475
/****************************
476476
Older stuff -
477477
****************************/
478+
#announcements {
479+
background-color: var(--lumo-warning-color);
480+
padding: var(--lumo-space-s) var(--lumo-space-m);
481+
text-align: left;
482+
}
483+
484+
.announcement {
485+
margin: var(--lumo-space-s) 0;
486+
display: flex;
487+
gap: var(--lumo-space-s);
488+
}
478489

490+
.announcement vaadin-icon {
491+
min-width: var(--icon-size-s);
492+
max-width: var(--icon-size-s);
493+
color: var(--icon-color-default);
494+
}
495+
496+
.announcement-text {
497+
display: inline;
498+
}
479499

480500
.heading-with-icon {
481501
color: #878787;

user-interface/frontend/themes/datamanager/components/main.css

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
grid-area: content-area;
33
}
44

5+
#announcements {
6+
grid-area: announcements;
7+
}
8+
59
#main-layout {
610
display: grid;
711
grid-template-columns: 1fr;
8-
grid-template-rows: minmax(max-content, 95%) minmax(min-content, 5%);
12+
grid-template-rows: auto minmax(max-content, 0.95fr) minmax(min-content, 0.05fr);
913
height: 100%;
1014
/*Default design of components is too big for smaller screens*/
1115
zoom: 90%;
1216
grid-template-areas:
17+
"announcements"
1318
"content-area"
1419
"data-manager-footer";
1520
}
@@ -273,8 +278,8 @@
273278
}
274279

275280
.main.project-overview {
276-
grid-template-columns: minmax(min-content, 1fr);
277-
grid-template-rows: minmax(min-content, 10%) minmax(min-content, 80%);
281+
grid-template-columns: 1fr;
282+
grid-template-rows: 0.1fr 0.9fr;
278283
grid-template-areas:
279284
"."
280285
"projectcollection";
@@ -484,14 +489,6 @@
484489
grid-template-rows: minmax(min-content, 60%) minmax(min-content, 20%) minmax(min-content, 20%);
485490
}
486491

487-
.main.project-overview {
488-
grid-template-columns: minmax(min-content, 1fr);
489-
grid-template-rows: minmax(min-content, 10%) minmax(min-content, 80%);
490-
grid-template-areas:
491-
"."
492-
"projectcollection";
493-
}
494-
495492
.main.raw-data {
496493
grid-template-columns: minmax(min-content, 1fr);
497494
grid-template-areas:
-167 KB
Binary file not shown.

user-interface/src/main/java/life/qbic/datamanager/AppConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
* @since 1.0.0
8686
*/
8787
@Configuration
88-
@ComponentScan({"life.qbic.identity.infrastructure"})
88+
@ComponentScan({"life.qbic.identity.infrastructure", "life.qbic.datamanager.announcements"})
8989
public class AppConfig {
9090
/*
9191
Wiring up identity application core and policies
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package life.qbic.datamanager.announcements;
2+
3+
import static java.util.Objects.nonNull;
4+
5+
import com.vaadin.flow.component.AttachEvent;
6+
import com.vaadin.flow.component.Component;
7+
import com.vaadin.flow.component.DetachEvent;
8+
import com.vaadin.flow.component.Html;
9+
import com.vaadin.flow.component.UI;
10+
import com.vaadin.flow.component.html.Div;
11+
import com.vaadin.flow.component.icon.VaadinIcon;
12+
import java.time.Duration;
13+
import java.time.Instant;
14+
import java.time.temporal.ChronoUnit;
15+
import java.util.List;
16+
import life.qbic.datamanager.announcements.AnnouncementService.Announcement;
17+
import org.apache.commons.logging.Log;
18+
import org.apache.commons.logging.LogFactory;
19+
import reactor.core.Disposable;
20+
import reactor.core.publisher.Flux;
21+
22+
public class AnnouncementComponent extends Div {
23+
24+
private static final Log log = LogFactory.getLog(AnnouncementComponent.class);
25+
private final transient AnnouncementService announcementService;
26+
private static final Duration INITIAL_DELAY = Duration.ZERO;
27+
private static final Duration REFRESH_INTERVAL = Duration.of(1, ChronoUnit.HOURS);
28+
private transient Disposable refreshRoutine;
29+
30+
31+
public AnnouncementComponent(AnnouncementService announcementService) {
32+
this.announcementService = announcementService;
33+
this.setId("announcements");
34+
}
35+
36+
private void subscribeToAnnouncements() {
37+
unsubscribeFromAnnouncements();
38+
UI ui = getUI().orElseThrow();
39+
refreshRoutine = Flux.interval(INITIAL_DELAY, REFRESH_INTERVAL)
40+
.doOnNext(
41+
it -> {
42+
ui.getSession().lock();
43+
log.debug("Fetching announcements for ui[%s] vaadin[%s] http[%s] ".formatted(
44+
ui.getUIId(), ui.getSession().getPushId(),
45+
ui.getSession().getSession().getId()));
46+
ui.getSession().unlock();
47+
})
48+
.flatMap(it -> announcementService.loadActiveAnnouncements(Instant.now())
49+
.collectList())
50+
.subscribe(announcements -> refreshAnnouncements(announcements, ui));
51+
}
52+
53+
private void unsubscribeFromAnnouncements() {
54+
if (nonNull(refreshRoutine)) {
55+
refreshRoutine.dispose();
56+
}
57+
}
58+
59+
private void refreshAnnouncements(List<Announcement> announcements, UI ui) {
60+
ui.access(() -> {
61+
this.removeAll();
62+
this.setVisible(!announcements.isEmpty());
63+
for (Announcement announcement : announcements) {
64+
add(renderAnnouncement(announcement));
65+
}
66+
});
67+
}
68+
69+
private Component renderAnnouncement(AnnouncementService.Announcement announcement) {
70+
Html html = new Html(
71+
"<div class=\"announcement-text\">%s</div>".formatted(announcement.message()));
72+
Div div = new Div(VaadinIcon.WRENCH.create(), html);
73+
div.addClassNames("announcement");
74+
return div;
75+
}
76+
77+
@Override
78+
protected void onAttach(AttachEvent attachEvent) {
79+
super.onAttach(attachEvent);
80+
subscribeToAnnouncements();
81+
}
82+
83+
@Override
84+
protected void onDetach(DetachEvent detachEvent) {
85+
unsubscribeFromAnnouncements();
86+
super.onDetach(detachEvent);
87+
}
88+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package life.qbic.datamanager.announcements;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.Table;
9+
import java.time.Instant;
10+
import java.util.List;
11+
import life.qbic.datamanager.announcements.AnnouncementRepository.Announcement;
12+
import org.springframework.data.repository.Repository;
13+
14+
@org.springframework.stereotype.Repository
15+
public interface AnnouncementRepository extends Repository<Announcement, Long> {
16+
17+
@Entity
18+
@Table(name = "announcements")
19+
class Announcement {
20+
21+
@Id
22+
@GeneratedValue(strategy = GenerationType.AUTO)
23+
@Column(name = "id", nullable = false)
24+
private Long id;
25+
@Column(name = "start_time")
26+
private Instant displayStartTime;
27+
@Column(name = "end_time")
28+
private Instant displayEndTime;
29+
@Column(name = "message")
30+
private String message;
31+
32+
public Long getId() {
33+
return id;
34+
}
35+
36+
public void setId(Long id) {
37+
this.id = id;
38+
}
39+
40+
public String getMessage() {
41+
return message;
42+
}
43+
}
44+
45+
List<Announcement> getAnnouncementByDisplayStartTimeBeforeAndDisplayEndTimeAfterOrderByDisplayStartTimeAsc(
46+
Instant min,
47+
Instant max);
48+
49+
50+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package life.qbic.datamanager.announcements;
2+
3+
import java.time.Instant;
4+
import reactor.core.publisher.Flux;
5+
6+
/**
7+
* Loads announcements
8+
*/
9+
public interface AnnouncementService {
10+
11+
/**
12+
* A {@link Flux} containing Announcements. Only publishes announcements that are valid given the
13+
* provided time. Published announcements are distinct until changed.
14+
*
15+
* @param timePoint the timepoint at which the announcement is valid
16+
* @return a {@link Flux} publishing announcements
17+
*/
18+
Flux<Announcement> loadActiveAnnouncements(Instant timePoint);
19+
20+
/**
21+
* An announcement with a given message.
22+
*
23+
* @param message
24+
*/
25+
record Announcement(String message) {
26+
27+
}
28+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package life.qbic.datamanager.announcements;
2+
3+
import java.time.Instant;
4+
import java.util.Objects;
5+
import life.qbic.datamanager.VirtualThreadScheduler;
6+
import org.springframework.stereotype.Service;
7+
import reactor.core.publisher.Flux;
8+
9+
@Service
10+
public class AnnouncementServiceImpl implements AnnouncementService {
11+
12+
private final AnnouncementRepository announcementRepository;
13+
14+
public AnnouncementServiceImpl(AnnouncementRepository announcementRepository) {
15+
this.announcementRepository = announcementRepository;
16+
}
17+
18+
@Override
19+
public Flux<Announcement> loadActiveAnnouncements(Instant timePoint) {
20+
return Flux.fromIterable(
21+
announcementRepository.getAnnouncementByDisplayStartTimeBeforeAndDisplayEndTimeAfterOrderByDisplayStartTimeAsc(
22+
timePoint, timePoint))
23+
.distinctUntilChanged(Objects::hashCode) //avoid unnecessary work
24+
.map(AnnouncementServiceImpl::toApiObject)
25+
.subscribeOn(VirtualThreadScheduler.getScheduler());
26+
}
27+
28+
private static Announcement toApiObject(AnnouncementRepository.Announcement announcement) {
29+
return new Announcement(announcement.getMessage());
30+
}
31+
}

user-interface/src/main/java/life/qbic/datamanager/configuration/DataManagementDatasourceConfig.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
@Configuration
4242
@EnableTransactionManagement
4343
@EnableJpaRepositories(
44-
basePackages = {"life.qbic.projectmanagement", "life.qbic.identity"},
44+
basePackages = {"life.qbic.projectmanagement", "life.qbic.identity",
45+
"life.qbic.datamanager.announcements"},
4546
entityManagerFactoryRef = "dataManagementEntityManagerFactory",
4647
transactionManagerRef = "dataManagementTransactionManager")
4748
public class DataManagementDatasourceConfig {
@@ -72,7 +73,8 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactory(
7273
@Qualifier("dataManagementDataSource") DataSource dataSource) {
7374
return builder
7475
.dataSource(dataSource)
75-
.packages("life.qbic.projectmanagement", "life.qbic.identity")
76+
.packages("life.qbic.projectmanagement", "life.qbic.identity",
77+
"life.qbic.datamanager.announcements")
7678
.properties(Map.of(
7779
"hibernate.hbm2ddl.auto", hibernateDdlAuto
7880
))

0 commit comments

Comments
 (0)