diff --git a/spring-boot-admin-server-ui/src/main/frontend/composables/useApplicationStore.ts b/spring-boot-admin-server-ui/src/main/frontend/composables/useApplicationStore.ts index d3043d3d0f9..6624ba990b3 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/composables/useApplicationStore.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/composables/useApplicationStore.ts @@ -16,7 +16,7 @@ export function createApplicationStore() { } type ApplicationStoreValue = { - applications: Ref>; + applications: Ref; applicationsInitialized: Ref; error: Ref; applicationStore: ApplicationStore; diff --git a/spring-boot-admin-server-ui/src/main/frontend/index.ts b/spring-boot-admin-server-ui/src/main/frontend/index.ts index 846762f5339..1ea9b4afabe 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/index.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/index.ts @@ -39,6 +39,7 @@ import views from './views'; import eventBus from '@/services/bus'; import sbaShell from '@/shell'; +import VueClickAwayPlugin from "vue3-click-away"; const applicationStore = createApplicationStore(); const viewRegistry = createViewRegistry(); @@ -128,6 +129,7 @@ const app = createApp({ app.use(i18n); app.use(components); +app.use(VueClickAwayPlugin); app.use(NotificationcenterPlugin, { duration: 10_000, }); diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instanceGroupService.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instanceGroupService.ts new file mode 100644 index 00000000000..37bcab2ebeb --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instanceGroupService.ts @@ -0,0 +1,44 @@ +import Instance from "@/services/instance"; +import {groupBy, sortBy, transform} from "lodash-es"; +import Application from "@/services/application"; + +const groupingFunctions = { + 'application': (instance: Instance) => instance.registration.name, + 'group': (instance: Instance) => instance.registration.metadata?.['group'] ?? "term.no_group", +} + +export type GroupingType = keyof typeof groupingFunctions; + +export type InstancesListType = { + name?: string; + statusKey?: string; + status?: string; + instances?: Instance[]; + applications?: Application[]; +} + +export const groupApplicationsBy = (applications: Application[], groupingFunction: GroupingType) => { + const instances = applications.flatMap(application => application.instances); + return groupInstancesBy(instances, groupingFunction); +} + +export const groupInstancesBy = (instances: Instance[], groupingFunction: GroupingType) => { + const grouped = groupBy( + instances, + groupingFunctions[groupingFunction] + ); + + const list = transform( + grouped, + (result, instances, name) => { + result.push({ + name, + instances: sortBy(instances, [ + (instance) => instance.registration.name, + ]), + }); + }, []); + + return sortBy(list, [(item) => item.status]); +} + diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/notification-filter.ts b/spring-boot-admin-server-ui/src/main/frontend/services/notification-filter.ts index 16d4fe8f5fb..ec41e590c7d 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/services/notification-filter.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/services/notification-filter.ts @@ -13,90 +13,85 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import moment from 'moment'; - import sbaConfig from '@/sba-config'; import axios from '@/utils/axios'; import uri from '@/utils/uri'; +import Application from "@/services/application"; +import Instance from "@/services/instance"; + +export type NotificationFilterProps = { + id: string, + applicationName: string, + instanceId: string, + expiry: string, + expired: boolean +} class NotificationFilter { - private id: string; - private applicationName: string; - private instanceId: string; - private expiry: moment.Moment | null; - - constructor({ expiry, ...filter }) { - Object.assign(this, filter); - this.expiry = expiry ? moment(expiry) : null; - } - - affects(obj) { - if (!obj) { - return false; + public readonly expired: boolean; + private readonly id: string; + private readonly applicationName: string; + private readonly instanceId: string; + + constructor({id, applicationName, instanceId, expiry, expired, ...filter}: NotificationFilterProps) { + Object.assign(this, filter); + this.id = id; + this.applicationName = applicationName; + this.instanceId = instanceId; + this.expired = expired; } - if (this.isApplicationFilter) { - return this.applicationName === obj.name; + static isSupported() { + return Boolean(sbaConfig.uiSettings.notificationFilterEnabled); } - if (this.isInstanceFilter) { - return this.instanceId === obj.id; + static async getFilters() { + return axios.get('notifications/filters', { + transformResponse: NotificationFilter._transformResponse, + }); } - return false; - } - - get isApplicationFilter() { - return this.applicationName != null; - } - - get isInstanceFilter() { - return this.instanceId != null; - } + static async addFilter(object: Instance | Application, ttl: number) { + const params = {ttl} as { ttl: number, applicationName?: string; instanceId?: string }; + if (object instanceof Application) { + params.applicationName = object.name; + } else if ('id' in object) { + params.instanceId = object.id; + } + return axios.post('notifications/filters', null, { + params, + transformResponse: NotificationFilter._transformResponse, + }); + } - async delete() { - return axios.delete(uri`notifications/filters/${this.id}`); - } + static _transformResponse(data: any) { + if (!data) { + return data; + } + const json = JSON.parse(data); + if (json instanceof Array) { + return json + .map((notificationFilter) => new NotificationFilter(notificationFilter)) + .filter((f) => !f.expired); + } + return new NotificationFilter(json); + } - static isSupported() { - return Boolean(sbaConfig.uiSettings.notificationFilterEnabled); - } + affects(obj: Instance | Application) { + if (!obj) { + return false; + } - static async getFilters() { - return axios.get('notifications/filters', { - transformResponse: NotificationFilter._transformResponse, - }); - } + if (obj instanceof Application) { + return this.applicationName === obj.name; + } - static async addFilter(object, ttl) { - const params = { ttl }; - if ('name' in object) { - params.applicationName = object.name; - } else if ('id' in object) { - params.instanceId = object.id; + return this.instanceId === obj.id; } - return axios.post('notifications/filters', null, { - params, - transformResponse: NotificationFilter._transformResponse, - }); - } - static _transformResponse(data) { - if (!data) { - return data; + async delete() { + return axios.delete(uri`notifications/filters/${this.id}`); } - const json = JSON.parse(data); - if (json instanceof Array) { - return json - .map(NotificationFilter._toNotificationFilters) - .filter((f) => !f.expired); - } - return NotificationFilter._toNotificationFilters(json); - } - - static _toNotificationFilters(notificationFilter) { - return new NotificationFilter(notificationFilter); - } } export default NotificationFilter; diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue b/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue index 22c1331f778..e4fa34636c3 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue @@ -69,10 +69,10 @@