diff --git a/@xen-orchestra/lite/.eslintrc.cjs b/@xen-orchestra/lite/.eslintrc.cjs index bda9cb6c5..7e6b870d9 100644 --- a/@xen-orchestra/lite/.eslintrc.cjs +++ b/@xen-orchestra/lite/.eslintrc.cjs @@ -12,15 +12,13 @@ module.exports = { "@vue/eslint-config-typescript/recommended", "@vue/eslint-config-prettier", ], - plugins: [ - "@limegrass/import-alias" - ], + plugins: ["@limegrass/import-alias"], rules: { "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-explicit-any": "off", "@limegrass/import-alias/import-alias": [ "error", - { aliasConfigPath: require("path").join(__dirname, "tsconfig.json") } + { aliasConfigPath: require("path").join(__dirname, "tsconfig.json") }, ], }, }; diff --git a/@xen-orchestra/lite/.prettierrc.cjs b/@xen-orchestra/lite/.prettierrc.cjs index bfa205e91..41d9f66db 100644 --- a/@xen-orchestra/lite/.prettierrc.cjs +++ b/@xen-orchestra/lite/.prettierrc.cjs @@ -11,4 +11,5 @@ module.exports = { ], importOrderSeparation: false, importOrderSortSpecifiers: true, + importOrderParserPlugins: ["typescript", "decorators-legacy"], }; diff --git a/@xen-orchestra/lite/package.json b/@xen-orchestra/lite/package.json index 10fdbf574..e22297d97 100644 --- a/@xen-orchestra/lite/package.json +++ b/@xen-orchestra/lite/package.json @@ -17,9 +17,17 @@ "@fortawesome/pro-thin-svg-icons": "^6.1.2", "@fortawesome/vue-fontawesome": "^3.0.1", "@novnc/novnc": "^1.3.0", + "@types/d3-time-format": "^4.0.0", + "@types/lodash-es": "^4.17.6", "@vueuse/core": "^8.7.5", "complex-matcher": "^0.7.0", + "d3-time-format": "^4.1.0", + "decorator-synchronized": "^0.6.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", "vue": "^3.2.37", "vue-router": "^4.0.16" diff --git a/@xen-orchestra/lite/src/composables/fetch-stats.composable.md b/@xen-orchestra/lite/src/composables/fetch-stats.composable.md new file mode 100644 index 000000000..36d198fec --- /dev/null +++ b/@xen-orchestra/lite/src/composables/fetch-stats.composable.md @@ -0,0 +1,23 @@ +# useFetchStats composable + +```vue + +``` diff --git a/@xen-orchestra/lite/src/composables/fetch-stats.composable.ts b/@xen-orchestra/lite/src/composables/fetch-stats.composable.ts new file mode 100644 index 000000000..81c99b765 --- /dev/null +++ b/@xen-orchestra/lite/src/composables/fetch-stats.composable.ts @@ -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( + 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> | undefined }; +} diff --git a/@xen-orchestra/lite/src/libs/utils.ts b/@xen-orchestra/lite/src/libs/utils.ts index 95ff44258..cd9c88133 100644 --- a/@xen-orchestra/lite/src/libs/utils.ts +++ b/@xen-orchestra/lite/src/libs/utils.ts @@ -1,3 +1,4 @@ +import { utcParse } from "d3-time-format"; import type { Filter } from "@/types/filter"; import { faSquareCheck } from "@fortawesome/pro-regular-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]; } + +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(); +} diff --git a/@xen-orchestra/lite/src/libs/xapi-stats.ts b/@xen-orchestra/lite/src/libs/xapi-stats.ts new file mode 100644 index 000000000..1e3da37a3 --- /dev/null +++ b/@xen-orchestra/lite/src/libs/xapi-stats.ts @@ -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; + iops: { + r: Record; + w: Record; + }; + memory: number[]; + memoryFree: number[]; + vifs: { + rx: Record; + tx: Record; + }; + xvds: { + w: Record; + r: Record; + }; +}; + +export type HostStats = { + cpus: Record; + ioThroughput: { + r: Record; + w: Record; + }; + iops: { + r: Record; + w: Record; + }; + iowait: Record; + latency: { + r: Record; + w: Record; + }; + load: number[]; + memory: number[]; + memoryFree: number[]; + pifs: { + rx: Record; + tx: Record; + }; +}; + +export type XapiStatsResponse = { + endTimestamp: number; + interval: number; + stats: T; +}; + +export default class XapiStats { + #xapi; + #statsByObject: { + [uuid: string]: { + [step: string]: XapiStatsResponse; + }; + } = {}; + 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: {}, + } + ); + } +} diff --git a/@xen-orchestra/lite/src/libs/xen-api.ts b/@xen-orchestra/lite/src/libs/xen-api.ts index 29613977a..9cd2cb30d 100644 --- a/@xen-orchestra/lite/src/libs/xen-api.ts +++ b/@xen-orchestra/lite/src/libs/xen-api.ts @@ -1,4 +1,5 @@ import { JSONRPCClient } from "json-rpc-2.0"; +import { parseDateTime } from "@/libs/utils"; export type RawObjectType = | "Bond" @@ -72,6 +73,7 @@ export interface XenApiPool extends XenApiRecord { } export interface XenApiHost extends XenApiRecord { + address: string; name_label: string; metrics: string; resident_VMs: string[]; @@ -190,6 +192,30 @@ export default class XenApi { 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( type: RawObjectType ): Promise> { diff --git a/@xen-orchestra/lite/src/stores/host.store.ts b/@xen-orchestra/lite/src/stores/host.store.ts index 8139c670f..ecc7868e6 100644 --- a/@xen-orchestra/lite/src/stores/host.store.ts +++ b/@xen-orchestra/lite/src/stores/host.store.ts @@ -1,8 +1,30 @@ import { defineStore } from "pinia"; import { sortRecordsByNameLabel } from "@/libs/utils"; +import type { GRANULARITY } from "@/libs/xapi-stats"; import type { XenApiHost } from "@/libs/xen-api"; import { createRecordContext } from "@/stores/index"; +import { useXenApiStore } from "@/stores/xen-api.store"; -export const useHostStore = defineStore("host", () => - createRecordContext("host", { sort: sortRecordsByNameLabel }) -); +export const useHostStore = defineStore("host", () => { + const xapiStats = useXenApiStore().getXapiStats(); + const recordContext = createRecordContext("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, + }; +}); diff --git a/@xen-orchestra/lite/src/stores/vm.store.ts b/@xen-orchestra/lite/src/stores/vm.store.ts index 65642e730..8f5372c0e 100644 --- a/@xen-orchestra/lite/src/stores/vm.store.ts +++ b/@xen-orchestra/lite/src/stores/vm.store.ts @@ -1,10 +1,15 @@ import { defineStore } from "pinia"; import { computed } from "vue"; import { sortRecordsByNameLabel } from "@/libs/utils"; +import type { GRANULARITY } from "@/libs/xapi-stats"; import type { XenApiVm } from "@/libs/xen-api"; +import { useHostStore } from "@/stores/host.store"; import { createRecordContext } from "@/stores/index"; +import { useXenApiStore } from "@/stores/xen-api.store"; export const useVmStore = defineStore("vm", () => { + const hostStore = useHostStore(); + const xapiStats = useXenApiStore().getXapiStats(); const baseVmContext = createRecordContext("VM", { filter: (vm) => !vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain, @@ -27,8 +32,26 @@ export const useVmStore = defineStore("vm", () => { 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 { ...baseVmContext, + getStats, opaqueRefsByHostRef, }; }); diff --git a/@xen-orchestra/lite/src/stores/xen-api.store.ts b/@xen-orchestra/lite/src/stores/xen-api.store.ts index 0b1bd7a62..e8936d7db 100644 --- a/@xen-orchestra/lite/src/stores/xen-api.store.ts +++ b/@xen-orchestra/lite/src/stores/xen-api.store.ts @@ -1,6 +1,7 @@ import { defineStore } from "pinia"; import { ref, watchEffect } from "vue"; import { useLocalStorage } from "@vueuse/core"; +import XapiStats from "@/libs/xapi-stats"; import type { XenApiRecord } from "@/libs/xen-api"; import XenApi from "@/libs/xen-api"; import { useConsoleStore } from "@/stores/console.store"; @@ -14,6 +15,7 @@ import { useVmStore } from "@/stores/vm.store"; export const useXenApiStore = defineStore("xen-api", () => { const xenApi = new XenApi(import.meta.env.VITE_XO_HOST); + const xapiStats = new XapiStats(xenApi); const currentSessionId = useLocalStorage("sessionId", null); const isConnected = ref(false); const isConnecting = ref(false); @@ -26,6 +28,14 @@ export const useXenApiStore = defineStore("xen-api", () => { return xenApi; } + function getXapiStats() { + if (!currentSessionId.value) { + throw new Error("Not connected to xapi"); + } + + return xapiStats; + } + async function init() { const poolStore = usePoolStore(); await poolStore.init(); @@ -116,6 +126,7 @@ export const useXenApiStore = defineStore("xen-api", () => { disconnect, init, getXapi, + getXapiStats, currentSessionId, }; }); diff --git a/@xen-orchestra/lite/src/types/decorator-synchronized.d.ts b/@xen-orchestra/lite/src/types/decorator-synchronized.d.ts new file mode 100644 index 000000000..fd246e7ff --- /dev/null +++ b/@xen-orchestra/lite/src/types/decorator-synchronized.d.ts @@ -0,0 +1,3 @@ +declare module "decorator-synchronized" { + export const synchronized: any; +} diff --git a/@xen-orchestra/lite/src/types/limit-concurrency-decorator.d.ts b/@xen-orchestra/lite/src/types/limit-concurrency-decorator.d.ts new file mode 100644 index 000000000..9dc88bdb3 --- /dev/null +++ b/@xen-orchestra/lite/src/types/limit-concurrency-decorator.d.ts @@ -0,0 +1,3 @@ +declare module "limit-concurrency-decorator" { + export const limitConcurrency: any; +} diff --git a/@xen-orchestra/lite/tsconfig.json b/@xen-orchestra/lite/tsconfig.json index 616f310ab..0fcd12304 100644 --- a/@xen-orchestra/lite/tsconfig.json +++ b/@xen-orchestra/lite/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@vue/tsconfig/tsconfig.web.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "compilerOptions": { + "experimentalDecorators": true, "lib": ["ES2019"], "baseUrl": ".", "paths": { diff --git a/yarn.lock b/yarn.lock index 95a49c567..5ccc21176 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2270,6 +2270,11 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105" 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": version "6.1.1" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz#3424ec6182515951816be9b11665d67efdce5b5f" @@ -2277,26 +2282,33 @@ dependencies: "@fortawesome/fontawesome-common-types" "6.1.1" -"@fortawesome/free-brands-svg-icons@^6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.1.1.tgz#3580961d4f42bd51dc171842402f23a18a5480b1" - integrity sha512-mFbI/czjBZ+paUtw5NPr2IXjun5KAC8eFqh1hnxowjA4mMZxWz4GCIksq6j9ZSa6Uxj9JhjjDVEd77p2LN2Blg== +"@fortawesome/pro-light-svg-icons@^6.1.2": + version "6.1.2" + resolved "https://npm.fontawesome.com/@fortawesome/pro-light-svg-icons/-/6.1.2/pro-light-svg-icons-6.1.2.tgz#90bbe97de6c9d921469de185abdcb8330e8eaf95" + integrity sha512-RPtgsmdIxj8tbmWCt+ulncl6DxgtrRD/EkBa8J9QPB/9++/nrIWyvybce8K7hWYzIIeYu+TZhSwGxtrBD0D6BA== dependencies: - "@fortawesome/fontawesome-common-types" "6.1.1" + "@fortawesome/fontawesome-common-types" "6.1.2" -"@fortawesome/free-regular-svg-icons@^6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.1.tgz#3f2f58262a839edf0643cbacee7a8a8230061c98" - integrity sha512-xXiW7hcpgwmWtndKPOzG+43fPH7ZjxOaoeyooptSztGmJxCAflHZxXNK0GcT0uEsR4jTGQAfGklDZE5NHoBhKg== +"@fortawesome/pro-regular-svg-icons@^6.1.2": + version "6.1.2" + resolved "https://npm.fontawesome.com/@fortawesome/pro-regular-svg-icons/-/6.1.2/pro-regular-svg-icons-6.1.2.tgz#f232ce529f59e8279536ce6fdd52f408e2ddf2b3" + integrity sha512-5BQ3Guv+FRDyXJ9qy5SQZc+tWsCstQscd4JILb8dFzFtZsDef9cDgxxFEj//QmyrVI45kKVhjfGY8voHHvMQTQ== dependencies: - "@fortawesome/fontawesome-common-types" "6.1.1" + "@fortawesome/fontawesome-common-types" "6.1.2" -"@fortawesome/free-solid-svg-icons@^6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.1.tgz#3369e673f8fe8be2fba30b1ec274d47490a830a6" - integrity sha512-0/5exxavOhI/D4Ovm2r3vxNojGZioPwmFrKg0ZUH69Q68uFhFPs6+dhAToh6VEQBntxPRYPuT5Cg1tpNa9JUPg== +"@fortawesome/pro-solid-svg-icons@^6.1.2": + version "6.1.2" + resolved "https://npm.fontawesome.com/@fortawesome/pro-solid-svg-icons/-/6.1.2/pro-solid-svg-icons-6.1.2.tgz#ecdab98c3e901c72e99c5a1a961070b3b80e05d6" + integrity sha512-E1TzmbuO0Klch/EqAjh4WPs93Oss8KwY5MUi2pcSyflN8sq5UqnMZFLWGlLQy24BXeMwgMDw4Aa9xH6lVNg1ig== 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": version "3.0.1" @@ -2917,6 +2929,11 @@ dependencies: "@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": version "4.17.31" 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" 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": version "10.0.3" 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" 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" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" 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: 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" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==