Skip to content

Commit

Permalink
feat(#1510): implement grouping of services in wallboard
Browse files Browse the repository at this point in the history
  • Loading branch information
SteKoe committed Oct 6, 2023
1 parent d2f064f commit cde7f95
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
:class="cssClasses"
:disabled="disabled === true"
@click="$emit('click', $event)"
:aria-label="$attrs.ariaLabel"
:title="$attrs.title"
>
<slot />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"fetching_data": "Lade Daten...",
"fetch_failed": "Abruf der Daten fehlgeschlagen.",
"float": "Float",
"group_by": {
"group": "Nach Gruppen gruppieren",
"application": "Nach Applikationen gruppieren"
},
"hours": "{count} Stunde | {count} Stunden",
"instance": "Instanz",
"instances": "Instanzen",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
"fetch_failed": "Fetching of data failed.",
"float": "Float",
"filter": "Filter",
"group_by": {
"group": "Group by group",
"application": "Group by application"
},
"hours": "{count} hour | {count} hours",
"instance": "Instance",
"instances": "Instances",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,13 @@ type Registration = {
}

type StatusInfo = {
status: string
status: Status
details: { [key: string]: string }
}

export type Status = "UNKNOWN" |
"OUT_OF_SERVICE" |
"UP" |
"DOWN" |
"OFFLINE" |
"RESTRICTED";
Original file line number Diff line number Diff line change
@@ -1,44 +1,76 @@
import Instance from "@/services/instance";
import {groupBy, sortBy, transform} from "lodash-es";
import Application from "@/services/application";
import {useApplicationStore} from "@/composables/useApplicationStore";

const groupingFunctions = {
'application': (instance: Instance) => instance.registration.name,
'group': (instance: Instance) => instance.registration.metadata?.['group'] ?? "term.no_group",
'application': (instance: Instance) => instance.registration.name,
'group': (instance: Instance) => instance.registration.metadata?.['group'] ?? "term.no_group",
}

export type GroupingType = keyof typeof groupingFunctions;
export const isGroupingType = (grouping: string = ""): grouping is GroupingType => {
return Object.keys(groupingFunctions).includes(grouping);
}

export type InstancesListType = {
name?: string;
statusKey?: string;
status?: string;
instances?: Instance[];
applications?: Application[];
export type InstancesListItem = {
name: string;
statusKey?: string;
status?: string;
instances: Instance[];
}

export const groupApplicationsBy = (applications: Application[], groupingFunction: GroupingType) => {
const instances = applications.flatMap(application => application.instances);
return groupInstancesBy(instances, groupingFunction);
const {applicationStore} = useApplicationStore();
const instances = applications.flatMap(application => application.instances);
const groupedInstances = groupInstancesBy(instances, groupingFunction);

if (groupingFunction === 'application') {
return groupedInstances
.map(item => {
return {
...applicationStore.findApplicationByInstanceId(item.instances[0].id),
...item,
}
});
}

return groupedInstances
.map(item => {
return {
status: getStatus(item.instances),
...item,
}
});
}

function getStatus(instances: Instance[]) {
if (instances.every(instance => instance.statusInfo.status === 'DOWN')) {
return 'DOWN';
}
if (instances.every(instance => instance.statusInfo.status === 'UP')) {
return 'UP';
}
return "RESTRICTED";
}

export const groupInstancesBy = (instances: Instance[], groupingFunction: GroupingType) => {
const grouped = groupBy<Instance>(
instances,
groupingFunctions[groupingFunction]
);

const list = transform<Instance[], InstancesListType[]>(
grouped,
(result, instances, name) => {
result.push({
name,
instances: sortBy(instances, [
(instance) => instance.registration.name,
]),
});
}, []);

return sortBy(list, [(item) => item.status]);
const grouped = groupBy<Instance>(
instances,
groupingFunctions[groupingFunction]
);

const list = transform<Instance[], InstancesListItem[]>(
grouped,
(result, instances, name) => {
result.push({
name,
instances: sortBy(instances, [
(instance) => instance.registration.name,
]),
});
}, []);

return sortBy(list, [(item) => item.status]);
}

2 changes: 1 addition & 1 deletion spring-boot-admin-server-ui/src/main/frontend/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default class ApplicationStore {
}
}

findApplicationByInstanceId(instanceId: string) {
findApplicationByInstanceId(instanceId: string): Application | undefined {
return findApplicationForInstance(this.applications, instanceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ import {ActionHandler, ApplicationActionHandler, InstanceActionHandler} from "@/
import {useI18n} from "vue-i18n";
import {useNotificationCenter} from "@stekoe/vue-toast-notificationcenter";
import {inject} from "@vue/runtime-core";
import {PropType} from "vue";
const $sbaModal = inject('$sbaModal');
const {t} = useI18n();
const notificationCenter = useNotificationCenter({});
const props = defineProps({
item: {
type: [Application, Instance],
type: Object as PropType<Application | Instance>,
required: true,
},
hasActiveNotificationFilter: {
Expand All @@ -80,7 +81,7 @@ if (props.item instanceof Application) {
name: 'journal',
query: {application: props.item.name},
};
} else if(props.item instanceof Instance) {
} else if (props.item instanceof Instance) {
actionHandler = new InstanceActionHandler($sbaModal, t, notificationCenter);
journalLink = {name: 'journal', query: {instanceId: props.item.id}};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@
<template v-else>
<div class="text-right mb-6" v-if="groupNames.length > 1">
<sba-button-group>
<sba-button @click="() => groupingCriterion = 'application'">
<sba-button @click="() => groupingCriterion = 'application'"
:aria-label="t('term.group_by.application')"
:title="t('term.group_by.application')">
<font-awesome-icon icon="list"/>
</sba-button>
<sba-button @click="() => groupingCriterion = 'group'">
<sba-button @click="() => groupingCriterion = 'group'"
:aria-label="t('term.group_by.group')"
:title="t('term.group_by.group')">
<font-awesome-icon icon="expand"/>
</sba-button>
</sba-button-group>
Expand All @@ -98,7 +102,7 @@
<template #actions v-if="isGroupedByApplication">
<ApplicationListItemAction
:has-notification-filters-support="hasNotificationFiltersSupport"
:item="applicationStore.findApplicationByInstanceId(group.instances[0].id)"
:item="applicationStore.findApplicationByInstanceId(group.instances[0].id)!"
@filter-settings="toggleNotificationFilterSettings"
/>
</template>
Expand Down Expand Up @@ -150,7 +154,7 @@ import Application from '@/services/application';
import NotificationFilter from '@/services/notification-filter';
import {anyValueMatches} from '@/utils/collections';
import {concatMap, mergeWith, Subject, timer} from '@/utils/rxjs';
import {groupApplicationsBy, GroupingType} from "@/services/instanceGroupService";
import {groupApplicationsBy, GroupingType, isGroupingType} from "@/services/instanceGroupService";
import ApplicationStats from "@/views/applications/ApplicationStats.vue";
import ApplicationNotificationCenter from "@/views/applications/ApplicationNotificationCenter.vue";
import ApplicationStatusHero from "@/views/applications/ApplicationStatusHero.vue";
Expand All @@ -171,8 +175,10 @@ const route = useRoute();
const {applications, applicationsInitialized, applicationStore} = useApplicationStore();
const notificationCenter = useNotificationCenter({});
const filter = ref(route.query.q?.toString());
const expandedGroups = ref([]);
const groupingCriterion = ref<GroupingType>('application');
const expandedGroups = ref([route.params.selected]);

const queryParamGroupBy = route.query?.groupBy?.toString();
const groupingCriterion = ref<GroupingType>(isGroupingType(queryParamGroupBy) ? queryParamGroupBy as GroupingType : 'application');
const notificationFilterSubject = new Subject();
const notificationFilters = ref([]);
const hasNotificationFiltersSupport = ref(NotificationFilter.isSupported());
Expand Down Expand Up @@ -220,16 +226,21 @@ watch(applicationsInitialized, (initialized) => {
}
})

let to = {
name: 'applications',
params: {selected: props.selected},
} as RouteLocationNamedRaw;

watch(filter, (q) => {
let to = {
name: 'applications',
name: 'wallboard',
params: {selected: props.selected},
} as RouteLocationNamedRaw;

if (q && q.length > 0) {
to = {
...to,
query: {
...route.query,
q
},
} as RouteLocationNamedRaw;
Expand All @@ -238,6 +249,20 @@ watch(filter, (q) => {
router.replace(to);
});

watch(groupingCriterion, (groupBy) => {
if (groupBy && groupBy.length > 0) {
to = {
...to,
query: {
...route.query,
groupBy
},
} as RouteLocationNamedRaw;
}

router.replace(to);
});

watch(groupNames, (newValue) => {
if (newValue.length === 1) {
groupingCriterion.value = 'application';
Expand Down
Loading

0 comments on commit cde7f95

Please sign in to comment.