feat(lite): xapiStat with fetchStats composable (#6361)
This commit is contained in:
parent
e03ff0a9be
commit
0b02c84e33
@ -12,15 +12,13 @@ module.exports = {
|
|||||||
"@vue/eslint-config-typescript/recommended",
|
"@vue/eslint-config-typescript/recommended",
|
||||||
"@vue/eslint-config-prettier",
|
"@vue/eslint-config-prettier",
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: ["@limegrass/import-alias"],
|
||||||
"@limegrass/import-alias"
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@limegrass/import-alias/import-alias": [
|
"@limegrass/import-alias/import-alias": [
|
||||||
"error",
|
"error",
|
||||||
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.json") }
|
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.json") },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -11,4 +11,5 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
importOrderSeparation: false,
|
importOrderSeparation: false,
|
||||||
importOrderSortSpecifiers: true,
|
importOrderSortSpecifiers: true,
|
||||||
|
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
||||||
};
|
};
|
||||||
|
@ -17,9 +17,17 @@
|
|||||||
"@fortawesome/pro-thin-svg-icons": "^6.1.2",
|
"@fortawesome/pro-thin-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.1",
|
"@fortawesome/vue-fontawesome": "^3.0.1",
|
||||||
"@novnc/novnc": "^1.3.0",
|
"@novnc/novnc": "^1.3.0",
|
||||||
|
"@types/d3-time-format": "^4.0.0",
|
||||||
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@vueuse/core": "^8.7.5",
|
"@vueuse/core": "^8.7.5",
|
||||||
"complex-matcher": "^0.7.0",
|
"complex-matcher": "^0.7.0",
|
||||||
|
"d3-time-format": "^4.1.0",
|
||||||
|
"decorator-synchronized": "^0.6.0",
|
||||||
"json-rpc-2.0": "^1.3.0",
|
"json-rpc-2.0": "^1.3.0",
|
||||||
|
"json5": "^2.2.1",
|
||||||
|
"limit-concurrency-decorator": "^0.5.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"make-error": "^1.3.6",
|
||||||
"pinia": "^2.0.14",
|
"pinia": "^2.0.14",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
"vue-router": "^4.0.16"
|
"vue-router": "^4.0.16"
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
# useFetchStats composable
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import useFetchStats from "@/composables/fetch-stats-composable";
|
||||||
|
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
|
||||||
|
|
||||||
|
const vmId = "1d381a66-d1cb-bb7e-50a1-feeab58b293d";
|
||||||
|
const hostId = "0aea61f4-c9d1-4060-94e8-4eb2024d082c";
|
||||||
|
|
||||||
|
const { stats: vmStats } = useFetchStats<VmStats>(
|
||||||
|
"vm",
|
||||||
|
vmId,
|
||||||
|
GRANULARITY.Seconds
|
||||||
|
);
|
||||||
|
|
||||||
|
const { stats: hostStats } = useFetchStats<HostStats>(
|
||||||
|
"host",
|
||||||
|
hostId,
|
||||||
|
GRANULARITY.Seconds
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
```
|
@ -0,0 +1,27 @@
|
|||||||
|
import { type Ref, ref } from "vue";
|
||||||
|
import { promiseTimeout, useIntervalFn } from "@vueuse/core";
|
||||||
|
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
|
||||||
|
import { useHostStore } from "@/stores/host.store";
|
||||||
|
import { useVmStore } from "@/stores/vm.store";
|
||||||
|
|
||||||
|
const STORES_BY_OBJECT_TYPE = {
|
||||||
|
host: useHostStore,
|
||||||
|
vm: useVmStore,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useFetchStats<T>(
|
||||||
|
type: "host" | "vm",
|
||||||
|
id: string,
|
||||||
|
granularity: GRANULARITY
|
||||||
|
) {
|
||||||
|
const stats = ref();
|
||||||
|
const fetch = STORES_BY_OBJECT_TYPE[type]().getStats;
|
||||||
|
|
||||||
|
const fetchStats = async () => {
|
||||||
|
stats.value = await fetch(id, granularity);
|
||||||
|
await promiseTimeout(stats.value.interval * 1000);
|
||||||
|
};
|
||||||
|
useIntervalFn(fetchStats);
|
||||||
|
|
||||||
|
return { stats } as { stats: Ref<XapiStatsResponse<T>> | undefined };
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { utcParse } from "d3-time-format";
|
||||||
import type { Filter } from "@/types/filter";
|
import type { Filter } from "@/types/filter";
|
||||||
import { faSquareCheck } from "@fortawesome/pro-regular-svg-icons";
|
import { faSquareCheck } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { faFont, faHashtag, faList } from "@fortawesome/pro-solid-svg-icons";
|
import { faFont, faHashtag, faList } from "@fortawesome/pro-solid-svg-icons";
|
||||||
@ -41,3 +42,13 @@ export function getFilterIcon(filter: Filter | undefined) {
|
|||||||
|
|
||||||
return iconsByType[filter.type];
|
return iconsByType[filter.type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseDateTime(dateTime: string) {
|
||||||
|
const date = utcParse("%Y%m%dT%H:%M:%SZ")(dateTime);
|
||||||
|
if (date === null) {
|
||||||
|
throw new RangeError(
|
||||||
|
`unable to parse XAPI datetime ${JSON.stringify(dateTime)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return date.getTime();
|
||||||
|
}
|
||||||
|
450
@xen-orchestra/lite/src/libs/xapi-stats.ts
Normal file
450
@xen-orchestra/lite/src/libs/xapi-stats.ts
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
import { synchronized } from "decorator-synchronized";
|
||||||
|
import JSON5 from "json5";
|
||||||
|
import { limitConcurrency } from "limit-concurrency-decorator";
|
||||||
|
import { defaults, findKey, forEach, identity, map } from "lodash-es";
|
||||||
|
import { BaseError } from "make-error";
|
||||||
|
import type XenApi from "@/libs/xen-api";
|
||||||
|
import type { XenApiHost } from "@/libs/xen-api";
|
||||||
|
|
||||||
|
class FaultyGranularity extends BaseError {}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// according to https://xapi-project.github.io/xen-api/metrics.html
|
||||||
|
// The values are stored at intervals of:
|
||||||
|
// - 5 seconds for the past 10 minutes
|
||||||
|
// - one minute for the past 2 hours
|
||||||
|
// - one hour for the past week
|
||||||
|
// - one day for the past year
|
||||||
|
enum RRD_STEP {
|
||||||
|
Seconds = 5,
|
||||||
|
Minutes = 60,
|
||||||
|
Hours = 3600,
|
||||||
|
Days = 86400,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GRANULARITY {
|
||||||
|
Seconds = "seconds",
|
||||||
|
Minutes = "minutes",
|
||||||
|
Hours = "hours",
|
||||||
|
Days = "days",
|
||||||
|
}
|
||||||
|
|
||||||
|
const RRD_STEP_FROM_STRING: { [key in GRANULARITY]: RRD_STEP } = {
|
||||||
|
[GRANULARITY.Seconds]: RRD_STEP.Seconds,
|
||||||
|
[GRANULARITY.Minutes]: RRD_STEP.Minutes,
|
||||||
|
[GRANULARITY.Hours]: RRD_STEP.Hours,
|
||||||
|
[GRANULARITY.Days]: RRD_STEP.Days,
|
||||||
|
};
|
||||||
|
|
||||||
|
// points = intervalInSeconds / step
|
||||||
|
const RRD_POINTS_PER_STEP: { [key in RRD_STEP]: number } = {
|
||||||
|
[RRD_STEP.Seconds]: 120,
|
||||||
|
[RRD_STEP.Minutes]: 120,
|
||||||
|
[RRD_STEP.Hours]: 168,
|
||||||
|
[RRD_STEP.Days]: 366,
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Utils
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
function convertNanToNull(value: number) {
|
||||||
|
return isNaN(value) ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Stats
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
const computeValues = (
|
||||||
|
dataRow: any,
|
||||||
|
legendIndex: number,
|
||||||
|
transformValue = identity
|
||||||
|
) =>
|
||||||
|
map(dataRow, ({ values }) =>
|
||||||
|
transformValue(convertNanToNull(values[legendIndex]))
|
||||||
|
);
|
||||||
|
|
||||||
|
const createGetProperty = (
|
||||||
|
obj: object,
|
||||||
|
property: string,
|
||||||
|
defaultValue: unknown
|
||||||
|
) => defaults(obj, { [property]: defaultValue })[property] as any;
|
||||||
|
|
||||||
|
const testMetric = (
|
||||||
|
test:
|
||||||
|
| string
|
||||||
|
| { exec: (type: string) => boolean }
|
||||||
|
| { (type: string): boolean },
|
||||||
|
type: string
|
||||||
|
): boolean =>
|
||||||
|
typeof test === "string"
|
||||||
|
? test === type
|
||||||
|
: typeof test === "function"
|
||||||
|
? test(type)
|
||||||
|
: test.exec(type);
|
||||||
|
|
||||||
|
const findMetric = (metrics: any, metricType: string) => {
|
||||||
|
let testResult;
|
||||||
|
let metric;
|
||||||
|
|
||||||
|
forEach(metrics, (current) => {
|
||||||
|
if (current.test === undefined) {
|
||||||
|
const newValues = findMetric(current, metricType);
|
||||||
|
|
||||||
|
metric = newValues.metric;
|
||||||
|
if (metric !== undefined) {
|
||||||
|
testResult = newValues.testResult;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ((testResult = testMetric(current.test, metricType))) {
|
||||||
|
metric = current;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { metric, testResult };
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The metrics:
|
||||||
|
// test: can be a function, regexp or string, default to: currentKey
|
||||||
|
// getPath: default to: () => currentKey
|
||||||
|
// transformValue: default to: identity
|
||||||
|
const STATS: { [key: string]: object } = {
|
||||||
|
host: {
|
||||||
|
load: {
|
||||||
|
test: "loadavg",
|
||||||
|
},
|
||||||
|
memoryFree: {
|
||||||
|
test: "memory_free_kib",
|
||||||
|
transformValue: (value: number) => value * 1024,
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
test: "memory_total_kib",
|
||||||
|
transformValue: (value: number) => value * 1024,
|
||||||
|
},
|
||||||
|
cpus: {
|
||||||
|
test: /^cpu(\d+)$/,
|
||||||
|
getPath: (matches: any) => ["cpus", matches[1]],
|
||||||
|
transformValue: (value: number) => value * 1e2,
|
||||||
|
},
|
||||||
|
pifs: {
|
||||||
|
rx: {
|
||||||
|
test: /^pif_eth(\d+)_rx$/,
|
||||||
|
getPath: (matches: unknown[]) => ["pifs", "rx", matches[1]],
|
||||||
|
},
|
||||||
|
tx: {
|
||||||
|
test: /^pif_eth(\d+)_tx$/,
|
||||||
|
getPath: (matches: unknown[]) => ["pifs", "tx", matches[1]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iops: {
|
||||||
|
r: {
|
||||||
|
test: /^iops_read_(\w+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["iops", "r", matches[1]],
|
||||||
|
},
|
||||||
|
w: {
|
||||||
|
test: /^iops_write_(\w+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["iops", "w", matches[1]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ioThroughput: {
|
||||||
|
r: {
|
||||||
|
test: /^io_throughput_read_(\w+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["ioThroughput", "r", matches[1]],
|
||||||
|
transformValue: (value: number) => value * 2 ** 20,
|
||||||
|
},
|
||||||
|
w: {
|
||||||
|
test: /^io_throughput_write_(\w+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["ioThroughput", "w", matches[1]],
|
||||||
|
transformValue: (value: number) => value * 2 ** 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
latency: {
|
||||||
|
r: {
|
||||||
|
test: /^read_latency_(\w+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["latency", "r", matches[1]],
|
||||||
|
transformValue: (value: number) => value / 1e3,
|
||||||
|
},
|
||||||
|
w: {
|
||||||
|
test: /^write_latency_(\w+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["latency", "w", matches[1]],
|
||||||
|
transformValue: (value: number) => value / 1e3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iowait: {
|
||||||
|
test: /^iowait_(\w+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["iowait", matches[1]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vm: {
|
||||||
|
memoryFree: {
|
||||||
|
test: "memory_internal_free",
|
||||||
|
transformValue: (value: number) => value * 1024,
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
test: (metricType: string) => metricType.endsWith("memory"),
|
||||||
|
},
|
||||||
|
cpus: {
|
||||||
|
test: /^cpu(\d+)$/,
|
||||||
|
getPath: (matches: unknown[]) => ["cpus", matches[1]],
|
||||||
|
transformValue: (value: number) => value * 1e2,
|
||||||
|
},
|
||||||
|
vifs: {
|
||||||
|
rx: {
|
||||||
|
test: /^vif_(\d+)_rx$/,
|
||||||
|
getPath: (matches: unknown[]) => ["vifs", "rx", matches[1]],
|
||||||
|
},
|
||||||
|
tx: {
|
||||||
|
test: /^vif_(\d+)_tx$/,
|
||||||
|
getPath: (matches: unknown[]) => ["vifs", "tx", matches[1]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xvds: {
|
||||||
|
r: {
|
||||||
|
test: /^vbd_xvd(.)_read$/,
|
||||||
|
getPath: (matches: unknown[]) => ["xvds", "r", matches[1]],
|
||||||
|
},
|
||||||
|
w: {
|
||||||
|
test: /^vbd_xvd(.)_write$/,
|
||||||
|
getPath: (matches: unknown[]) => ["xvds", "w", matches[1]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
iops: {
|
||||||
|
r: {
|
||||||
|
test: /^vbd_xvd(.)_iops_read$/,
|
||||||
|
getPath: (matches: unknown[]) => ["iops", "r", matches[1]],
|
||||||
|
},
|
||||||
|
w: {
|
||||||
|
test: /^vbd_xvd(.)_iops_write$/,
|
||||||
|
getPath: (matches: unknown[]) => ["iops", "w", matches[1]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// RRD
|
||||||
|
// json: {
|
||||||
|
// meta: {
|
||||||
|
// start: Number,
|
||||||
|
// step: Number,
|
||||||
|
// end: Number,
|
||||||
|
// rows: Number,
|
||||||
|
// columns: Number,
|
||||||
|
// legend: String[rows]
|
||||||
|
// },
|
||||||
|
// data: Item[columns] // Item = { t: Number, values: Number[rows] }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Local cache
|
||||||
|
// _statsByObject : {
|
||||||
|
// [uuid]: {
|
||||||
|
// [step]: {
|
||||||
|
// endTimestamp: Number, // the timestamp of the last statistic point
|
||||||
|
// interval: Number, // step
|
||||||
|
// stats: T
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
export type VmStats = {
|
||||||
|
cpus: Record<string, number[]>;
|
||||||
|
iops: {
|
||||||
|
r: Record<string, number[]>;
|
||||||
|
w: Record<string, number[]>;
|
||||||
|
};
|
||||||
|
memory: number[];
|
||||||
|
memoryFree: number[];
|
||||||
|
vifs: {
|
||||||
|
rx: Record<string, number[]>;
|
||||||
|
tx: Record<string, number[]>;
|
||||||
|
};
|
||||||
|
xvds: {
|
||||||
|
w: Record<string, number[]>;
|
||||||
|
r: Record<string, number[]>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HostStats = {
|
||||||
|
cpus: Record<string, number[]>;
|
||||||
|
ioThroughput: {
|
||||||
|
r: Record<string, number[]>;
|
||||||
|
w: Record<string, number[]>;
|
||||||
|
};
|
||||||
|
iops: {
|
||||||
|
r: Record<string, number[]>;
|
||||||
|
w: Record<string, number[]>;
|
||||||
|
};
|
||||||
|
iowait: Record<string, number[]>;
|
||||||
|
latency: {
|
||||||
|
r: Record<string, number[]>;
|
||||||
|
w: Record<string, number[]>;
|
||||||
|
};
|
||||||
|
load: number[];
|
||||||
|
memory: number[];
|
||||||
|
memoryFree: number[];
|
||||||
|
pifs: {
|
||||||
|
rx: Record<string, number[]>;
|
||||||
|
tx: Record<string, number[]>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type XapiStatsResponse<T> = {
|
||||||
|
endTimestamp: number;
|
||||||
|
interval: number;
|
||||||
|
stats: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class XapiStats {
|
||||||
|
#xapi;
|
||||||
|
#statsByObject: {
|
||||||
|
[uuid: string]: {
|
||||||
|
[step: string]: XapiStatsResponse<HostStats | any>;
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
constructor(xapi: XenApi) {
|
||||||
|
this.#xapi = xapi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute one http request on a XenServer for get stats
|
||||||
|
// Return stats (Json format) or throws got exception
|
||||||
|
@limitConcurrency(3)
|
||||||
|
async _getJson(host: XenApiHost, timestamp: any, step: any) {
|
||||||
|
const resp = await this.#xapi.getResource("/rrd_updates", {
|
||||||
|
host,
|
||||||
|
query: {
|
||||||
|
cf: "AVERAGE",
|
||||||
|
host: "true",
|
||||||
|
interval: step,
|
||||||
|
json: "true",
|
||||||
|
start: timestamp,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return JSON5.parse(await resp.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid multiple requests, we keep a cache for the stats and
|
||||||
|
// only return it if we not exceed a step
|
||||||
|
#getCachedStats(uuid: any, step: any, currentTimeStamp: any) {
|
||||||
|
const statsByObject = this.#statsByObject;
|
||||||
|
|
||||||
|
const stats = statsByObject[uuid]?.[step];
|
||||||
|
if (stats === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.endTimestamp + step < currentTimeStamp) {
|
||||||
|
delete statsByObject[uuid][step];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
@synchronized.withKey(({ host }: { host: XenApiHost }) => host.uuid)
|
||||||
|
async _getAndUpdateStats({
|
||||||
|
host,
|
||||||
|
uuid,
|
||||||
|
granularity,
|
||||||
|
}: {
|
||||||
|
host: XenApiHost;
|
||||||
|
uuid: any;
|
||||||
|
granularity: GRANULARITY;
|
||||||
|
}) {
|
||||||
|
const step =
|
||||||
|
granularity === undefined
|
||||||
|
? RRD_STEP.Seconds
|
||||||
|
: RRD_STEP_FROM_STRING[granularity];
|
||||||
|
if (step === undefined) {
|
||||||
|
throw new FaultyGranularity(
|
||||||
|
`Unknown granularity: '${granularity}'. Use 'seconds', 'minutes', 'hours', or 'days'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const currentTimeStamp = await this.#xapi.getHostServertime(host);
|
||||||
|
|
||||||
|
const stats = this.#getCachedStats(uuid, step, currentTimeStamp);
|
||||||
|
if (stats !== undefined) {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxDuration = step * RRD_POINTS_PER_STEP[step];
|
||||||
|
|
||||||
|
// To avoid crossing over the boundary, we ask for one less step
|
||||||
|
const optimumTimestamp = currentTimeStamp - maxDuration + step;
|
||||||
|
const json = await this._getJson(host, optimumTimestamp, step);
|
||||||
|
|
||||||
|
const actualStep = json.meta.step as number;
|
||||||
|
|
||||||
|
if (json.data.length > 0) {
|
||||||
|
// fetched data is organized from the newest to the oldest
|
||||||
|
// but this implementation requires it in the other direction
|
||||||
|
json.data.reverse();
|
||||||
|
json.meta.legend.forEach((legend: any, index: number) => {
|
||||||
|
const [, type, uuid, metricType] = /^AVERAGE:([^:]+):(.+):(.+)$/.exec(
|
||||||
|
legend
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
const metrics = STATS[type] as any;
|
||||||
|
if (metrics === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metric, testResult } = findMetric(metrics, metricType) as any;
|
||||||
|
if (metric === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xoObjectStats = createGetProperty(this.#statsByObject, uuid, {});
|
||||||
|
let stepStats = xoObjectStats[actualStep];
|
||||||
|
if (
|
||||||
|
stepStats === undefined ||
|
||||||
|
stepStats.endTimestamp !== json.meta.end
|
||||||
|
) {
|
||||||
|
stepStats = xoObjectStats[actualStep] = {
|
||||||
|
endTimestamp: json.meta.end,
|
||||||
|
interval: actualStep,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const path =
|
||||||
|
metric.getPath !== undefined
|
||||||
|
? metric.getPath(testResult)
|
||||||
|
: [findKey(metrics, metric)];
|
||||||
|
|
||||||
|
const lastKey = path.length - 1;
|
||||||
|
let metricStats = createGetProperty(stepStats, "stats", {});
|
||||||
|
path.forEach((property: any, key: number) => {
|
||||||
|
if (key === lastKey) {
|
||||||
|
metricStats[property] = computeValues(
|
||||||
|
json.data,
|
||||||
|
index,
|
||||||
|
metric.transformValue
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
metricStats = createGetProperty(metricStats, property, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualStep !== step) {
|
||||||
|
throw new FaultyGranularity(
|
||||||
|
`Unable to get the true granularity: ${actualStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.#statsByObject[uuid]?.[step] ?? {
|
||||||
|
endTimestamp: currentTimeStamp,
|
||||||
|
interval: step,
|
||||||
|
stats: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { JSONRPCClient } from "json-rpc-2.0";
|
import { JSONRPCClient } from "json-rpc-2.0";
|
||||||
|
import { parseDateTime } from "@/libs/utils";
|
||||||
|
|
||||||
export type RawObjectType =
|
export type RawObjectType =
|
||||||
| "Bond"
|
| "Bond"
|
||||||
@ -72,6 +73,7 @@ export interface XenApiPool extends XenApiRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface XenApiHost extends XenApiRecord {
|
export interface XenApiHost extends XenApiRecord {
|
||||||
|
address: string;
|
||||||
name_label: string;
|
name_label: string;
|
||||||
metrics: string;
|
metrics: string;
|
||||||
resident_VMs: string[];
|
resident_VMs: string[];
|
||||||
@ -190,6 +192,30 @@ export default class XenApi {
|
|||||||
return this.#client.request(method, args);
|
return this.#client.request(method, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getHostServertime(host: XenApiHost) {
|
||||||
|
const serverLocaltime = (await this.#call("host.get_servertime", [
|
||||||
|
this.sessionId,
|
||||||
|
host.$ref,
|
||||||
|
])) as string;
|
||||||
|
return Math.floor(parseDateTime(serverLocaltime) / 1e3);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResource(
|
||||||
|
pathname: string,
|
||||||
|
{ host, query }: { host: XenApiHost; query: any }
|
||||||
|
) {
|
||||||
|
const url = new URL("http://localhost");
|
||||||
|
url.protocol = window.location.protocol;
|
||||||
|
url.hostname = host.address;
|
||||||
|
url.pathname = pathname;
|
||||||
|
url.search = new URLSearchParams({
|
||||||
|
...query,
|
||||||
|
session_id: this.#sessionId,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
return fetch(url);
|
||||||
|
}
|
||||||
|
|
||||||
async loadRecords<T extends XenApiRecord>(
|
async loadRecords<T extends XenApiRecord>(
|
||||||
type: RawObjectType
|
type: RawObjectType
|
||||||
): Promise<Map<string, T>> {
|
): Promise<Map<string, T>> {
|
||||||
|
@ -1,8 +1,30 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { sortRecordsByNameLabel } from "@/libs/utils";
|
import { sortRecordsByNameLabel } from "@/libs/utils";
|
||||||
|
import type { GRANULARITY } from "@/libs/xapi-stats";
|
||||||
import type { XenApiHost } from "@/libs/xen-api";
|
import type { XenApiHost } from "@/libs/xen-api";
|
||||||
import { createRecordContext } from "@/stores/index";
|
import { createRecordContext } from "@/stores/index";
|
||||||
|
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||||
|
|
||||||
export const useHostStore = defineStore("host", () =>
|
export const useHostStore = defineStore("host", () => {
|
||||||
createRecordContext<XenApiHost>("host", { sort: sortRecordsByNameLabel })
|
const xapiStats = useXenApiStore().getXapiStats();
|
||||||
);
|
const recordContext = createRecordContext<XenApiHost>("host", {
|
||||||
|
sort: sortRecordsByNameLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getStats(id: string, granularity: GRANULARITY) {
|
||||||
|
const host = recordContext.getRecordByUuid(id);
|
||||||
|
if (host === undefined) {
|
||||||
|
throw new Error(`Host ${id} could not be found.`);
|
||||||
|
}
|
||||||
|
return xapiStats._getAndUpdateStats({
|
||||||
|
host,
|
||||||
|
uuid: host.uuid,
|
||||||
|
granularity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...recordContext,
|
||||||
|
getStats,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { sortRecordsByNameLabel } from "@/libs/utils";
|
import { sortRecordsByNameLabel } from "@/libs/utils";
|
||||||
|
import type { GRANULARITY } from "@/libs/xapi-stats";
|
||||||
import type { XenApiVm } from "@/libs/xen-api";
|
import type { XenApiVm } from "@/libs/xen-api";
|
||||||
|
import { useHostStore } from "@/stores/host.store";
|
||||||
import { createRecordContext } from "@/stores/index";
|
import { createRecordContext } from "@/stores/index";
|
||||||
|
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||||
|
|
||||||
export const useVmStore = defineStore("vm", () => {
|
export const useVmStore = defineStore("vm", () => {
|
||||||
|
const hostStore = useHostStore();
|
||||||
|
const xapiStats = useXenApiStore().getXapiStats();
|
||||||
const baseVmContext = createRecordContext<XenApiVm>("VM", {
|
const baseVmContext = createRecordContext<XenApiVm>("VM", {
|
||||||
filter: (vm) =>
|
filter: (vm) =>
|
||||||
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain,
|
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain,
|
||||||
@ -27,8 +32,26 @@ export const useVmStore = defineStore("vm", () => {
|
|||||||
return vmsOpaqueRefsByHostOpaqueRef;
|
return vmsOpaqueRefsByHostOpaqueRef;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function getStats(id: string, granularity: GRANULARITY) {
|
||||||
|
const vm = baseVmContext.getRecordByUuid(id);
|
||||||
|
if (vm === undefined) {
|
||||||
|
throw new Error(`VM ${id} could not be found.`);
|
||||||
|
}
|
||||||
|
const host = hostStore.getRecord(vm.resident_on);
|
||||||
|
if (host === undefined) {
|
||||||
|
throw new Error(`VM ${id} is halted or host could not be found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return xapiStats._getAndUpdateStats({
|
||||||
|
host,
|
||||||
|
uuid: vm.uuid,
|
||||||
|
granularity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...baseVmContext,
|
...baseVmContext,
|
||||||
|
getStats,
|
||||||
opaqueRefsByHostRef,
|
opaqueRefsByHostRef,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, watchEffect } from "vue";
|
import { ref, watchEffect } from "vue";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
import XapiStats from "@/libs/xapi-stats";
|
||||||
import type { XenApiRecord } from "@/libs/xen-api";
|
import type { XenApiRecord } from "@/libs/xen-api";
|
||||||
import XenApi from "@/libs/xen-api";
|
import XenApi from "@/libs/xen-api";
|
||||||
import { useConsoleStore } from "@/stores/console.store";
|
import { useConsoleStore } from "@/stores/console.store";
|
||||||
@ -14,6 +15,7 @@ import { useVmStore } from "@/stores/vm.store";
|
|||||||
|
|
||||||
export const useXenApiStore = defineStore("xen-api", () => {
|
export const useXenApiStore = defineStore("xen-api", () => {
|
||||||
const xenApi = new XenApi(import.meta.env.VITE_XO_HOST);
|
const xenApi = new XenApi(import.meta.env.VITE_XO_HOST);
|
||||||
|
const xapiStats = new XapiStats(xenApi);
|
||||||
const currentSessionId = useLocalStorage<string | null>("sessionId", null);
|
const currentSessionId = useLocalStorage<string | null>("sessionId", null);
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
const isConnecting = ref(false);
|
const isConnecting = ref(false);
|
||||||
@ -26,6 +28,14 @@ export const useXenApiStore = defineStore("xen-api", () => {
|
|||||||
return xenApi;
|
return xenApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getXapiStats() {
|
||||||
|
if (!currentSessionId.value) {
|
||||||
|
throw new Error("Not connected to xapi");
|
||||||
|
}
|
||||||
|
|
||||||
|
return xapiStats;
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const poolStore = usePoolStore();
|
const poolStore = usePoolStore();
|
||||||
await poolStore.init();
|
await poolStore.init();
|
||||||
@ -116,6 +126,7 @@ export const useXenApiStore = defineStore("xen-api", () => {
|
|||||||
disconnect,
|
disconnect,
|
||||||
init,
|
init,
|
||||||
getXapi,
|
getXapi,
|
||||||
|
getXapiStats,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
3
@xen-orchestra/lite/src/types/decorator-synchronized.d.ts
vendored
Normal file
3
@xen-orchestra/lite/src/types/decorator-synchronized.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare module "decorator-synchronized" {
|
||||||
|
export const synchronized: any;
|
||||||
|
}
|
3
@xen-orchestra/lite/src/types/limit-concurrency-decorator.d.ts
vendored
Normal file
3
@xen-orchestra/lite/src/types/limit-concurrency-decorator.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare module "limit-concurrency-decorator" {
|
||||||
|
export const limitConcurrency: any;
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
"lib": ["ES2019"],
|
"lib": ["ES2019"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
63
yarn.lock
63
yarn.lock
@ -2270,6 +2270,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105"
|
||||||
integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA==
|
integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA==
|
||||||
|
|
||||||
|
"@fortawesome/fontawesome-common-types@6.1.2":
|
||||||
|
version "6.1.2"
|
||||||
|
resolved "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.1.2/fontawesome-common-types-6.1.2.tgz#c1095b1bbabf19f37f9ff0719db38d92a410bcfe"
|
||||||
|
integrity sha512-wBaAPGz1Awxg05e0PBRkDRuTsy4B3dpBm+zreTTyd9TH4uUM27cAL4xWyWR0rLJCrRwzVsQ4hF3FvM6rqydKPA==
|
||||||
|
|
||||||
"@fortawesome/fontawesome-svg-core@^6.1.1":
|
"@fortawesome/fontawesome-svg-core@^6.1.1":
|
||||||
version "6.1.1"
|
version "6.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz#3424ec6182515951816be9b11665d67efdce5b5f"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz#3424ec6182515951816be9b11665d67efdce5b5f"
|
||||||
@ -2277,26 +2282,33 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-common-types" "6.1.1"
|
"@fortawesome/fontawesome-common-types" "6.1.1"
|
||||||
|
|
||||||
"@fortawesome/free-brands-svg-icons@^6.1.1":
|
"@fortawesome/pro-light-svg-icons@^6.1.2":
|
||||||
version "6.1.1"
|
version "6.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.1.1.tgz#3580961d4f42bd51dc171842402f23a18a5480b1"
|
resolved "https://npm.fontawesome.com/@fortawesome/pro-light-svg-icons/-/6.1.2/pro-light-svg-icons-6.1.2.tgz#90bbe97de6c9d921469de185abdcb8330e8eaf95"
|
||||||
integrity sha512-mFbI/czjBZ+paUtw5NPr2IXjun5KAC8eFqh1hnxowjA4mMZxWz4GCIksq6j9ZSa6Uxj9JhjjDVEd77p2LN2Blg==
|
integrity sha512-RPtgsmdIxj8tbmWCt+ulncl6DxgtrRD/EkBa8J9QPB/9++/nrIWyvybce8K7hWYzIIeYu+TZhSwGxtrBD0D6BA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-common-types" "6.1.1"
|
"@fortawesome/fontawesome-common-types" "6.1.2"
|
||||||
|
|
||||||
"@fortawesome/free-regular-svg-icons@^6.1.1":
|
"@fortawesome/pro-regular-svg-icons@^6.1.2":
|
||||||
version "6.1.1"
|
version "6.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.1.tgz#3f2f58262a839edf0643cbacee7a8a8230061c98"
|
resolved "https://npm.fontawesome.com/@fortawesome/pro-regular-svg-icons/-/6.1.2/pro-regular-svg-icons-6.1.2.tgz#f232ce529f59e8279536ce6fdd52f408e2ddf2b3"
|
||||||
integrity sha512-xXiW7hcpgwmWtndKPOzG+43fPH7ZjxOaoeyooptSztGmJxCAflHZxXNK0GcT0uEsR4jTGQAfGklDZE5NHoBhKg==
|
integrity sha512-5BQ3Guv+FRDyXJ9qy5SQZc+tWsCstQscd4JILb8dFzFtZsDef9cDgxxFEj//QmyrVI45kKVhjfGY8voHHvMQTQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-common-types" "6.1.1"
|
"@fortawesome/fontawesome-common-types" "6.1.2"
|
||||||
|
|
||||||
"@fortawesome/free-solid-svg-icons@^6.1.1":
|
"@fortawesome/pro-solid-svg-icons@^6.1.2":
|
||||||
version "6.1.1"
|
version "6.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.1.tgz#3369e673f8fe8be2fba30b1ec274d47490a830a6"
|
resolved "https://npm.fontawesome.com/@fortawesome/pro-solid-svg-icons/-/6.1.2/pro-solid-svg-icons-6.1.2.tgz#ecdab98c3e901c72e99c5a1a961070b3b80e05d6"
|
||||||
integrity sha512-0/5exxavOhI/D4Ovm2r3vxNojGZioPwmFrKg0ZUH69Q68uFhFPs6+dhAToh6VEQBntxPRYPuT5Cg1tpNa9JUPg==
|
integrity sha512-E1TzmbuO0Klch/EqAjh4WPs93Oss8KwY5MUi2pcSyflN8sq5UqnMZFLWGlLQy24BXeMwgMDw4Aa9xH6lVNg1ig==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-common-types" "6.1.1"
|
"@fortawesome/fontawesome-common-types" "6.1.2"
|
||||||
|
|
||||||
|
"@fortawesome/pro-thin-svg-icons@^6.1.2":
|
||||||
|
version "6.1.2"
|
||||||
|
resolved "https://npm.fontawesome.com/@fortawesome/pro-thin-svg-icons/-/6.1.2/pro-thin-svg-icons-6.1.2.tgz#20dd695caffccc27bf983e6a89b5404588b2767d"
|
||||||
|
integrity sha512-X4zq1SPqs6wK3QcK/6maLUVfqKHdx5hh9IwBreDCZfCncJH09stmpVEU+BvjlhxXGgt2Fb8v+UJbz410z3XOtg==
|
||||||
|
dependencies:
|
||||||
|
"@fortawesome/fontawesome-common-types" "6.1.2"
|
||||||
|
|
||||||
"@fortawesome/vue-fontawesome@^3.0.1":
|
"@fortawesome/vue-fontawesome@^3.0.1":
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
@ -2917,6 +2929,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/d3-time-format@^4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946"
|
||||||
|
integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==
|
||||||
|
|
||||||
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
|
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
|
||||||
version "4.17.31"
|
version "4.17.31"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f"
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f"
|
||||||
@ -2997,6 +3014,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9"
|
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9"
|
||||||
integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==
|
integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==
|
||||||
|
|
||||||
|
"@types/lodash-es@^4.17.6":
|
||||||
|
version "4.17.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0"
|
||||||
|
integrity sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*":
|
||||||
|
version "4.14.182"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
|
||||||
|
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
|
||||||
|
|
||||||
"@types/markdown-it@^10.0.0":
|
"@types/markdown-it@^10.0.0":
|
||||||
version "10.0.3"
|
version "10.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.3.tgz#a9800d14b112c17f1de76ec33eff864a4815eec7"
|
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.3.tgz#a9800d14b112c17f1de76ec33eff864a4815eec7"
|
||||||
@ -12912,7 +12941,7 @@ lodash-compat@^3.10.1:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash-compat/-/lodash-compat-3.10.2.tgz#c6940128a9d30f8e902cd2cf99fd0cba4ecfc183"
|
resolved "https://registry.yarnpkg.com/lodash-compat/-/lodash-compat-3.10.2.tgz#c6940128a9d30f8e902cd2cf99fd0cba4ecfc183"
|
||||||
integrity sha512-k8SE/OwvWfYZqx3MA/Ry1SHBDWre8Z8tCs0Ba0bF5OqVNvymxgFZ/4VDtbTxzTvcoG11JpTMFsaeZp/yGYvFnA==
|
integrity sha512-k8SE/OwvWfYZqx3MA/Ry1SHBDWre8Z8tCs0Ba0bF5OqVNvymxgFZ/4VDtbTxzTvcoG11JpTMFsaeZp/yGYvFnA==
|
||||||
|
|
||||||
lodash-es@^4.2.1:
|
lodash-es@^4.17.21, lodash-es@^4.2.1:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||||
@ -13241,7 +13270,7 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^6.0.0"
|
semver "^6.0.0"
|
||||||
|
|
||||||
make-error@^1, make-error@^1.0.2, make-error@^1.0.4, make-error@^1.2.1, make-error@^1.3.0, make-error@^1.3.2, make-error@^1.3.5:
|
make-error@^1, make-error@^1.0.2, make-error@^1.0.4, make-error@^1.2.1, make-error@^1.3.0, make-error@^1.3.2, make-error@^1.3.5, make-error@^1.3.6:
|
||||||
version "1.3.6"
|
version "1.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||||
|
Loading…
Reference in New Issue
Block a user