diff --git a/.gitignore b/.gitignore index 9035d791a..2c01b7ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ dist ### STORAGE ### data/user-* data/system +# Ignore all projects but life-cycle +data/shared-* +!data/shared-lifecycle HELP.md target/ diff --git a/armadillo/src/main/java/org/molgenis/armadillo/info/FileMetrics.java b/armadillo/src/main/java/org/molgenis/armadillo/info/FileMetrics.java new file mode 100644 index 000000000..805c3f7b2 --- /dev/null +++ b/armadillo/src/main/java/org/molgenis/armadillo/info/FileMetrics.java @@ -0,0 +1,45 @@ +package org.molgenis.armadillo.info; + +import io.micrometer.common.lang.NonNull; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import java.io.File; +import org.springframework.stereotype.Component; + +@Component +public class FileMetrics implements MeterBinder { + + private static final String PATH = System.getProperty("user.dir"); + + @Override + public void bindTo(@NonNull MeterRegistry registry) { + File folder = new File(PATH); + File[] listOfFiles = folder.listFiles(); + + int fileCount = 0; + int dirCount = 0; + + if (listOfFiles != null) { + for (File file : listOfFiles) { + if (file.isFile()) { + fileCount++; + } else if (file.isDirectory()) { + dirCount++; + } + } + } + + Gauge.builder("user.files.count", fileCount, Integer::doubleValue) + .description("Number of files in the current directory") + .baseUnit("files") + .tags("path", PATH) + .register(registry); + + Gauge.builder("user.directories.count", dirCount, Integer::doubleValue) + .description("Number of directories in the current directory") + .baseUnit("directories") + .tags("path", PATH) + .register(registry); + } +} diff --git a/armadillo/src/main/java/org/molgenis/armadillo/info/RMetrics.java b/armadillo/src/main/java/org/molgenis/armadillo/info/RMetrics.java index 81f787304..cdf39a8dd 100644 --- a/armadillo/src/main/java/org/molgenis/armadillo/info/RMetrics.java +++ b/armadillo/src/main/java/org/molgenis/armadillo/info/RMetrics.java @@ -13,7 +13,7 @@ public class RMetrics { @Bean - MeterBinder rProcesses(ProfileService profileService, RProcessEndpoint processes) { + MeterBinder rProcesses(ProfileService profileService, RProcessEndpoint rProcessEndpoint) { return registry -> runAsSystem( @@ -24,7 +24,7 @@ MeterBinder rProcesses(ProfileService profileService, RProcessEndpoint processes environment -> Gauge.builder( "rserve.processes.current", - () -> processes.countRServeProcesses(environment)) + () -> rProcessEndpoint.countRServeProcesses(environment)) .tag("environment", environment) .description( "Current number of RServe processes on the R environment") diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index a853f07d3..2c46aefe5 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -1,5 +1,5 @@ import { ApiError } from "@/helpers/errors"; -import { sanitizeObject } from "@/helpers/utils"; +import { objectDeepCopy, sanitizeObject } from "@/helpers/utils"; import { Principal, Profile, @@ -8,7 +8,11 @@ import { Auth, RemoteFileInfo, RemoteFileDetail, + Metric, + HalResponse, + Metrics, } from "@/types/api"; + import { ObjectWithStringKey, StringArray } from "@/types/types"; import { APISettings } from "./config"; @@ -88,7 +92,7 @@ export async function handleResponse(response: Response) { } } -export async function getActuator() { +export async function getActuator(): Promise { let result = await get("/actuator"); return result; } @@ -103,6 +107,59 @@ export async function getVersion() { return result.build.version; } +/** + * Fetch all metric values on one go using the list. + */ +export async function getMetricsAll(): Promise { + const paths = await getMetrics(); + const metrics: Metrics = {}; + + await Promise.all( + paths.map(async (path) => { + const response = await getMetric(path); + metrics[path] = response; + return { path: response }; + }) + ); + + return metrics; +} + +/** + * Get list of metric IDs in as dictionary keys. + */ +async function getMetrics(): Promise { + const path = "/actuator/metrics"; + return await get(path) + .then((data) => { + // Check if the data has 'names' property + if (data.hasOwnProperty("names")) { + return data.names; + } else { + console.log("No names found in the data"); + return []; + } + }) + .catch((error) => { + console.error(`Error fetching ${path}`, error); + return {}; + }); +} + +/** + * Fetches give Metric ID. + * + * @path: dot separated string + * + * Example: a.b.c + */ +async function getMetric(id: string): Promise { + const path = `/actuator/metrics/${id}`; + return get(path).then((data) => { + return objectDeepCopy(data); + }); +} + export async function deleteUser(email: string) { return delete_("/access/users", email); } diff --git a/ui/src/components/Actuator.vue b/ui/src/components/Actuator.vue new file mode 100644 index 000000000..66d3242f4 --- /dev/null +++ b/ui/src/components/Actuator.vue @@ -0,0 +1,175 @@ + + + diff --git a/ui/src/components/ActuatorItem.vue b/ui/src/components/ActuatorItem.vue new file mode 100644 index 000000000..241817a21 --- /dev/null +++ b/ui/src/components/ActuatorItem.vue @@ -0,0 +1,56 @@ + + + diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index ecaf1500b..85f521803 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -149,3 +149,12 @@ export function sanitizeObject( export function isEmptyObject(obj: Object) { return Object.keys(obj).length === 0; } + +/** + * Get a cloned version of the input. + * + * @param input:Object + */ +export function objectDeepCopy(input: T): T { + return JSON.parse(JSON.stringify(input)); +} diff --git a/ui/src/types/api.d.ts b/ui/src/types/api.d.ts index 7dc4039c2..65c88dab4 100644 --- a/ui/src/types/api.d.ts +++ b/ui/src/types/api.d.ts @@ -1,4 +1,4 @@ -import { StringArray } from "@/types/types"; +import { Dictionary, StringArray } from "@/types/types"; export type Project = { name: string; @@ -73,3 +73,46 @@ export type Profile = { }; export type Auth = { user: string; pwd: string }; + +/** + * Types for /actuator response + * + * Seems HAL API + */ +interface ActuatorLink { + href: string; + templated?: boolean; +} + +interface HalLinks { + [key: string]: ActuatorLink; +} + +export interface HalResponse { + _links: HalLinks; +} + +/** + * Types for /actuator/metric response. + */ +type Measurement = { + statistic: string; + value: number; +}; + +type AvailableTag = { + tag: string; + values: string[]; +}; + +export type Metric = { + name: string; + description: string; + baseUnit: string; + measurements: Measurement[]; + availableTags: AvailableTag[]; + searchWords?: string; + _display?: boolean; +}; + +export type Metrics = Dictionary; diff --git a/ui/src/types/types.d.ts b/ui/src/types/types.d.ts index d6ebd7caf..bec052a6f 100644 --- a/ui/src/types/types.d.ts +++ b/ui/src/types/types.d.ts @@ -28,6 +28,10 @@ export type TypeString = | "object"; export type StringArray = string[]; +interface Dictionary { + [key: string]: T; +} + // Maybe later expand with float/int/enum/character export type TypeObject = Record; export type ProjectsExplorerData = { diff --git a/ui/src/views/Insight.vue b/ui/src/views/Insight.vue index 87e8df97f..460ed486c 100644 --- a/ui/src/views/Insight.vue +++ b/ui/src/views/Insight.vue @@ -1,19 +1,37 @@ diff --git a/ui/src/views/Users.vue b/ui/src/views/Users.vue index 344bc7d37..a3a9cf012 100644 --- a/ui/src/views/Users.vue +++ b/ui/src/views/Users.vue @@ -124,6 +124,7 @@ import SearchBar from "@/components/SearchBar.vue"; import Table from "@/components/Table.vue"; import InlineRowEdit from "@/components/InlineRowEdit.vue"; import FeedbackMessage from "@/components/FeedbackMessage.vue"; + import { deleteUser, getUsers, putUser, getProjects } from "@/api/api"; import { sortAlphabetically, stringIncludesOtherString } from "@/helpers/utils"; import { defineComponent, onMounted, Ref, ref } from "vue"; diff --git a/ui/vite.config.js b/ui/vite.config.js index 639c316c3..0e93732c2 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -25,6 +25,20 @@ export default defineConfig({ port: 8080, }, }, + "^/actuator$": { + target: { + protocol: "http:", + host: "localhost", + port: 8080, + }, + }, + "^/actuator/.*": { + target: { + protocol: "http:", + host: "localhost", + port: 8080, + }, + }, "^/insight/.*": { target: { protocol: "http:",