feat(lite): xapiStat with fetchStats composable (#6361)

This commit is contained in:
Mathieu 2022-08-15 09:59:22 +02:00 committed by Julien Fontanet
parent e03ff0a9be
commit 0b02c84e33
15 changed files with 660 additions and 24 deletions

View File

@ -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") },
], ],
}, },
}; };

View File

@ -11,4 +11,5 @@ module.exports = {
], ],
importOrderSeparation: false, importOrderSeparation: false,
importOrderSortSpecifiers: true, importOrderSortSpecifiers: true,
importOrderParserPlugins: ["typescript", "decorators-legacy"],
}; };

View File

@ -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"

View File

@ -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>
```

View File

@ -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 };
}

View File

@ -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();
}

View 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: {},
}
);
}
}

View File

@ -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>> {

View File

@ -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,
};
});

View File

@ -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,
}; };
}); });

View File

@ -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,
}; };
}); });

View File

@ -0,0 +1,3 @@
declare module "decorator-synchronized" {
export const synchronized: any;
}

View File

@ -0,0 +1,3 @@
declare module "limit-concurrency-decorator" {
export const limitConcurrency: any;
}

View File

@ -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": {

View File

@ -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==