Skip to content

Commit 6dae843

Browse files
authored
feat(#1396): implement filter and search function for wallboard (#2612)
closes #1396
1 parent 42e2271 commit 6dae843

File tree

8 files changed

+171
-45
lines changed

8 files changed

+171
-45
lines changed

spring-boot-admin-server-ui/package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spring-boot-admin-server-ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,16 @@
4040
"d3-time": "3.1.0",
4141
"event-source-polyfill": "1.0.31",
4242
"file-saver": "2.0.5",
43+
"fuse.js": "^6.6.2",
4344
"iso8601-duration": "2.1.1",
4445
"lodash-es": "4.17.21",
4546
"mitt": "^3.0.0",
4647
"moment": "2.29.4",
4748
"popper.js": "1.16.1",
4849
"pretty-bytes": "6.1.0",
50+
"random-string": "0.2.0",
4951
"react": "18.2.0",
5052
"react-dom": "18.2.0",
51-
"random-string": "0.2.0",
5253
"resize-observer-polyfill": "1.5.1",
5354
"rxjs": "7.8.1",
5455
"uuid": "9.0.0",

spring-boot-admin-server-ui/src/main/frontend/components/sba-input.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
:readonly="readonly"
5454
:type="type"
5555
:value="modelValue"
56-
class="focus:z-10 p-2 relative flex-1 block w-full rounded-none sm:text-sm bg-opacity-40 backdrop-blur-sm"
56+
class="focus:z-10 p-2 relative flex-1 block w-full rounded-none bg-opacity-40 backdrop-blur-sm"
5757
@input="handleInput"
5858
/>
5959
<!-- APPEND -->

spring-boot-admin-server-ui/src/main/frontend/components/sba-loading-spinner.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="flex text-center h-full">
2+
<div class="flex text-center h-full items-center">
33
<svg
44
:class="classNames(sizeClassNames)"
55
class="inline text-gray-900 animate-spin"

spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.de.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"term": {
66
"affects_all_instances": "Betrifft alle {count} Instanzen",
77
"affects_this_instance_only": "Betrifft nur diese Instanz",
8+
"all": "Alle",
89
"application": "Anwendung",
910
"attributes": "Attribute",
1011
"bytes": "Bytes",
@@ -45,6 +46,18 @@
4546
"go_to_previous_page": "Gehe zur vorherigen Seite",
4647
"go_to_page_n": "Gehe zu Seite {page}",
4748
"current_page": "Seite {page}, aktuelle Seite",
48-
"go_to_next_page": "Gehe zur nächsten Seite"
49+
"go_to_next_page": "Gehe zur nächsten Seite",
50+
"no_results_for_term": "Keine Ergebnisse für \"{term}\"."
51+
},
52+
"health": {
53+
"label": "Zustand",
54+
"status": {
55+
"DOWN": "down",
56+
"UP": "up",
57+
"RESTRICTED": "eingeschränkt",
58+
"UNKNOWN": "unbekannt",
59+
"OUT_OF_SERVICE": "außer Betrieb",
60+
"OFFLINE": "offline"
61+
}
4962
}
5063
}

spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.en.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"term": {
66
"affects_all_instances": "Affects all {count} instances",
77
"affects_this_instance_only": "Affects this instance only",
8+
"all": "all",
89
"application": "Application",
910
"applications_tc": "{n} application | {n} applications",
1011
"attributes": "Attributes",
@@ -53,6 +54,18 @@
5354
"go_to_previous_page": "Go to previous page",
5455
"go_to_page_n": "Go to page {page}",
5556
"current_page": "Page {page}, current page",
56-
"go_to_next_page": "Go to next page"
57+
"go_to_next_page": "Go to next page",
58+
"no_results_for_term": "No results for \"{term}\"."
59+
},
60+
"health": {
61+
"label": "Health status",
62+
"status": {
63+
"DOWN": "down",
64+
"UP": "up",
65+
"RESTRICTED": "restricted",
66+
"UNKNOWN": "unknown",
67+
"OUT_OF_SERVICE": "out of service",
68+
"OFFLINE": "offline"
69+
}
5770
}
5871
}

spring-boot-admin-server-ui/src/main/frontend/store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export const findApplicationForInstance = (
5050
);
5151
};
5252

53-
type ApplicationStoreListener = () => void;
53+
type NoopListener = () => void;
54+
type ApplicationAddedListener = (newApplications: Application[]) => void;
55+
type ApplicationStoreListener = NoopListener | ApplicationAddedListener;
5456

5557
export default class ApplicationStore {
5658
private _listeners: { [p: string]: Array<ApplicationStoreListener> } = {};

spring-boot-admin-server-ui/src/main/frontend/views/wallboard/index.vue

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,36 @@
1616

1717
<template>
1818
<section class="wallboard section">
19+
<div class="flex gap-2 justify-end absolute w-full md:w-[28rem] top-14 right-0 bg-black/20 py-3 px-4 rounded-bl">
20+
<sba-input
21+
class="flex-1"
22+
v-model="termFilter"
23+
:placeholder="$t('term.filter')"
24+
name="filter"
25+
type="search"
26+
>
27+
<template #prepend>
28+
<font-awesome-icon icon="filter"/>
29+
</template>
30+
</sba-input>
31+
32+
<select
33+
v-if="healthStatus.length > 1"
34+
v-model="statusFilter"
35+
class="relative focus:z-10 focus:ring-indigo-500 focus:border-indigo-500 block sm:text-sm border-gray-300 rounded"
36+
>
37+
<option selected value="none" v-text="$t('term.all')"/>
38+
<optgroup :label="t('health.label')">
39+
<option
40+
v-for="status in healthStatus"
41+
:key="status"
42+
:value="status"
43+
v-text="t('health.status.' + status)"
44+
/>
45+
</optgroup>
46+
</select>
47+
</div>
48+
1949
<sba-alert
2050
v-if="error"
2151
:error="error"
@@ -24,57 +54,115 @@
2454
severity="WARN"
2555
/>
2656

27-
<p
57+
<sba-loading-spinner
2858
v-if="!applicationsInitialized"
29-
class="is-muted is-loading"
30-
v-text="t('applications.loading_applications')"
3159
/>
32-
<hex-mesh
33-
v-if="applicationsInitialized"
34-
:class-for-item="classForApplication"
35-
:items="applications"
36-
@click="select"
37-
>
38-
<template #item="{ item: application }">
39-
<div :key="application.name" class="hex__body application">
40-
<div class="application__status-indicator" />
41-
<div class="application__header application__time-ago is-muted">
42-
<sba-time-ago :date="application.statusTimestamp" />
43-
</div>
44-
<div class="application__body">
45-
<h1 class="application__name" v-text="application.name" />
46-
<p
47-
class="application__instances is-muted"
48-
v-text="
49-
t('wallboard.instances_count', application.instances.length)
50-
"
60+
61+
<template v-if="applicationsInitialized">
62+
<div class="flex w-full h-full items-center text-center text-white text-xl"
63+
v-if="termFilter.length > 0 && applications.length === 0"
64+
v-text="t('term.no_results_for_term', {
65+
term: termFilter
66+
})"/>
67+
<hex-mesh
68+
v-if="applicationsInitialized"
69+
:class-for-item="classForApplication"
70+
:items="applications"
71+
@click="select"
72+
>
73+
<template #item="{ item: application }">
74+
<div :key="application.name" class="hex__body application">
75+
<div class="application__status-indicator"/>
76+
<div class="application__header application__time-ago is-muted">
77+
<sba-time-ago :date="application.statusTimestamp"/>
78+
</div>
79+
<div class="application__body">
80+
<h1 class="application__name" v-text="application.name"/>
81+
<p
82+
class="application__instances is-muted"
83+
v-text="
84+
t('wallboard.instances_count', application.instances.length)
85+
"
86+
/>
87+
</div>
88+
<h2
89+
class="application__footer application__version"
90+
v-text="application.buildVersion"
5191
/>
5292
</div>
53-
<h2
54-
class="application__footer application__version"
55-
v-text="application.buildVersion"
56-
/>
57-
</div>
58-
</template>
59-
</hex-mesh>
93+
</template>
94+
</hex-mesh>
95+
</template>
6096
</section>
6197
</template>
6298

6399
<script>
64-
import { useI18n } from 'vue-i18n';
100+
import Fuse from 'fuse.js';
101+
import {computed, ref} from 'vue';
102+
import {useI18n} from 'vue-i18n';
65103
66-
import { HealthStatus } from '@/HealthStatus';
67-
import { useApplicationStore } from '@/composables/useApplicationStore';
104+
import {HealthStatus} from '@/HealthStatus';
105+
import {useApplicationStore} from '@/composables/useApplicationStore';
68106
import hexMesh from '@/views/wallboard/hex-mesh';
69107
70108
export default {
71-
components: { hexMesh },
109+
components: {hexMesh},
72110
setup() {
73-
const { t } = useI18n();
111+
const {t} = useI18n();
112+
const termFilter = ref('');
113+
const statusFilter = ref('none');
74114
75-
const { applications, applicationsInitialized, error } =
115+
const {applications, applicationsInitialized, error} =
76116
useApplicationStore();
77-
return { applications, applicationsInitialized, error, t };
117+
118+
const fuse = computed(
119+
() =>
120+
new Fuse(applications.value, {
121+
includeScore: true,
122+
useExtendedSearch: true,
123+
threshold: 0.25,
124+
keys: ['name', 'buildVersion', 'instances.name', 'instances.id'],
125+
})
126+
);
127+
128+
const filteredApplications = computed(() => {
129+
function filterByTerm() {
130+
if (termFilter.value.length > 0) {
131+
return fuse.value.search(termFilter.value).map((sr) => sr.item);
132+
} else {
133+
return applications.value;
134+
}
135+
}
136+
137+
function filterByStatus(result) {
138+
if (statusFilter.value !== 'none') {
139+
return result.filter(
140+
(application) => application.status === statusFilter.value
141+
);
142+
}
143+
144+
return result;
145+
}
146+
147+
let result = filterByTerm();
148+
result = filterByStatus(result);
149+
150+
return result;
151+
});
152+
153+
const healthStatus = computed(() => {
154+
return applications.value.map((application) => application.status);
155+
});
156+
157+
return {
158+
applications: filteredApplications,
159+
applicationsInitialized,
160+
error,
161+
t,
162+
termFilter,
163+
statusFilter,
164+
healthStatus,
165+
};
78166
},
79167
methods: {
80168
classForApplication(application) {
@@ -105,17 +193,17 @@ export default {
105193
if (application.instances.length === 1) {
106194
this.$router.push({
107195
name: 'instances/details',
108-
params: { instanceId: application.instances[0].id },
196+
params: {instanceId: application.instances[0].id},
109197
});
110198
} else {
111199
this.$router.push({
112200
name: 'applications',
113-
params: { selected: application.name },
201+
params: {selected: application.name},
114202
});
115203
}
116204
},
117205
},
118-
install({ viewRegistry }) {
206+
install({viewRegistry}) {
119207
viewRegistry.addView({
120208
path: '/wallboard',
121209
name: 'wallboard',

0 commit comments

Comments
 (0)