Add playbook editor (#702)

This commit is contained in:
Jonas Plum
2023-01-21 23:25:55 +01:00
committed by GitHub
parent ee9d906e28
commit 27f1b0df79
10 changed files with 1656 additions and 109 deletions

View File

@@ -9,6 +9,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@crinkles/digl": "^2.0.2",
"@koumoul/vjsf": "2.21.1",
"@mdi/font": "7.1.96",
"@mdi/util": "0.3.2",
@@ -23,6 +24,7 @@
"axios": "1.2.2",
"chart.js": "2.9.4",
"core-js": "3.27.2",
"d3": "^7.8.0",
"graphlib": "2.1.8",
"json-schema-editor-vue": "2.2.1",
"just-kebab-case": "4.2.0",
@@ -30,6 +32,7 @@
"less-loader": "11.1.0",
"lodash": "4.17.21",
"luxon": "3.2.1",
"panzoom": "^9.4.3",
"register-service-worker": "1.7.2",
"splitpanes": "2.4.1",
"swagger-ui": "4.13.0",

View File

@@ -0,0 +1,273 @@
<template>
<v-card data-app outlined>
<v-card-title class="d-flex">
<span>Edit Task</span>
<v-spacer/>
<v-dialog v-model="deleteDialog" max-width="400">
<template #activator="{ on }">
<v-btn outlined v-on="on" class="mr-2" small>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">Delete Task</v-card-title>
<v-card-text>Are you sure you want to delete this task?</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn color="blue darken-1" text @click="deleteDialog = false">Cancel</v-btn>
<v-btn color="blue darken-1" text @click="deleteTask">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-btn @click="close" outlined small>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-text-field
v-model="task.name"
label="Name"
variant="underlined"/>
<v-textarea
v-model="task.description"
label="Description"
auto-grow
rows="1"
variant="underlined"/>
<v-select
v-model="task.type"
:items="['input','automation','task']"
label="Type"
variant="underlined"/>
<AdvancedJSONSchemaEditor
v-if="task.type === 'input'"
:schema="task.schema"
@save="task.schema = JSON.parse($event)" />
<v-select
v-if="task.type === 'automation'"
v-model="task.automation"
:items="automations"
item-text="id"
item-value="id"
label="Automation"
variant="underlined"/>
<v-list v-if="task.type === 'automation'">
<v-subheader class="pa-0" style="padding-inline-start: 0 !important;">Payload Mapping</v-subheader>
<v-toolbar v-for="(expr, key) in task.payload" :key="key" class="next-row" flat dense>
{{ key }}:
<v-text-field
v-model="task.payload[key]"
label="Expression"
variant="solo"
clearable
hide-details
density="compact"
bg-color="surface"
/>
<v-btn @click="deletePayloadMapping(key)" color="error" class="pa-0 ma-0" icon>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-toolbar>
<v-toolbar class="next-row" flat dense>
<v-text-field
v-model="newPayloadMapping"
label="Payload Field"
variant="solo"
bg-color="surface"
hide-details
density="compact"
/>:
<v-text-field
v-model="newExpression"
label="CAQL Expression"
variant="solo"
hide-details
density="compact"
bg-color="surface"
/>
<v-btn
@click="addPayloadMapping"
:disabled="!newPayloadMapping || !newExpression"
class="pa-0 ma-0"
icon>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-toolbar>
</v-list>
<v-list v-if="task.next || possibleNexts.length > 1">
<v-subheader class="pa-0" style="padding-inline-start: 0 !important;">Next Task(s)</v-subheader>
<v-toolbar v-for="(expr, key) in task.next" :key="key" class="next-row" flat dense>
If
<v-text-field
v-model="task.next[key]"
label="Condition (leave empty to always run)"
variant="solo"
clearable
hide-details
density="compact"
bg-color="surface"
/>
run
<span class="font-weight-black">
{{ playbook.tasks[key].name }}
</span>
<v-btn @click="deleteNext(key)" color="error" class="pa-0 ma-0" icon>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-toolbar>
<v-toolbar v-if="possibleNexts.length > 0" class="next-row" flat dense>
If
<v-text-field
v-model="newCondition"
label="Condition (leave empty to always run)"
variant="solo"
clearable
hide-details
density="compact"
bg-color="surface"
/>
run
<v-select
v-model="newNext"
item-text="name"
item-value="key"
:items="possibleNexts"
variant="solo"
bg-color="surface"
hide-details
density="compact"
/>
<v-btn
@click="addNext"
:disabled="!newNext"
class="pa-0 ma-0"
icon>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-toolbar>
</v-list>
<v-switch
v-if="parents.length > 1"
label="Join (Require all previous tasks to be completed)"
v-model="task.join"
color="primary"/>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import {defineProps, ref, watch, defineEmits, del, set, onMounted, computed} from "vue";
import AdvancedJSONSchemaEditor from "@/components/AdvancedJSONSchemaEditor.vue";
import {API} from "@/services/api";
import {AutomationResponse} from "@/client";
interface Task {
name: string;
description: string;
type: string;
next: Record<string, string>;
payload: Record<string, string>;
join: boolean;
}
const props = defineProps<{
value: Task;
possibleNexts: Array<Record<string, string>>;
parents: Array<string>;
playbook: object;
}>();
const emit = defineEmits(["input", "delete", "close"]);
const deleteDialog = ref(false);
const deleteTask = () => emit("delete");
const close = () => {
emit("close");
};
const task = ref(props.value);
watch(() => props.value, (value) => {
task.value = value;
});
watch(task, (value) => {
emit("input", value);
});
// const task = computed({
// get: () => {
// console.log("get", props.value);
// return props.value;
// },
// set: (value) => {
// console.log("set", value);
// emit("input", value);
// }
// });
const deleteNext = (key: string) => {
del(task.value.next, key);
};
const deletePayloadMapping = (key: string) => {
del(task.value.payload, key);
};
const newNext = ref('');
const newCondition = ref('');
const newPayloadMapping = ref('');
const newExpression = ref('');
watch(() => props.possibleNexts, () => {
if (props.possibleNexts.length > 0) {
newNext.value = props.possibleNexts[0].key;
}
}, {deep: true, immediate: true});
const addNext = () => {
if (task.value.next === undefined) {
// task.value.next = {};
set(task.value, 'next', {});
}
// task.value.next[newNext.value] = newCondition.value;
set(task.value.next, newNext.value, newCondition.value);
newNext.value = "";
newCondition.value = "";
};
const addPayloadMapping = () => {
if (task.value.payload === undefined) {
// task.value.payload = {};
set(task.value, 'payload', {});
}
// task.value.payload[newPayloadMapping.value] = newExpression.value;
set(task.value.payload, newPayloadMapping.value, newExpression.value);
newPayloadMapping.value = "";
newExpression.value = "";
};
const automations = ref<Array<AutomationResponse>>([]);
onMounted(() => {
API.listAutomations().then((response) => {
automations.value = response.data;
});
});
</script>
<style>
.next-row {
padding-top: 10px;
background: none !important;
}
.next-row .v-toolbar__content {
gap: 5px;
padding: 0;
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<v-card outlined>
<v-card-title>
Create a new step
<v-spacer />
<v-btn @click="close" outlined small>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-form v-model="valid">
<v-text-field
v-model="newTask.name"
label="Name"
:rules="[val => (val || '').length > 0 || 'This field is required']"
variant="underlined"/>
<v-textarea
v-model="newTask.description"
label="Description"
auto-grow
rows="1"
variant="underlined"/>
<v-text-field
v-model="newTask.key"
label="Key (generated automatically)"
readonly
disabled
:rules="[val => (val || '').length > 0 || 'This field is required']"
variant="underlined"/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn color="primary" @click="createTask" :disabled="!valid">Create</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import {defineProps, ref, watch, defineEmits} from "vue";
const props = defineProps<{
playbook: any;
}>();
const valid = ref(false);
const newTask = ref({
name: '',
description: '',
key: '',
next: {},
});
watch(newTask, (val) => {
if (val.name) {
const newKeyBase = val.name.toLowerCase().replace(/ /g, '_');
let newKey = newKeyBase;
if (!(newKey in props.playbook.tasks)) {
newTask.value.key = newKey;
} else {
let i = 1;
while (newKey in props.playbook.tasks) {
newKey = newKeyBase + '_' + i;
i++;
}
newTask.value.key = newKey;
}
}
}, {deep: true});
const emit = defineEmits(["createTask", "close"]);
const createTask = () => {
emit('createTask', newTask.value);
};
const close = () => {
emit("close");
};
</script>

View File

@@ -0,0 +1,164 @@
<template>
<div id="graphwrapper">
<div id="gab">
<slot name="actionbar"/>
</div>
<v-toolbar
id="gtb"
class="ma-2"
floating
dense
>
<v-btn @click.prevent.stop="reset" title="Reset" icon>
<v-icon color="#000">mdi-image-filter-center-focus</v-icon>
</v-btn>
<v-btn @click.prevent.stop="zoomIn" title="Zoom in" :disabled="isMaxZoom" icon>
<v-icon color="#000">mdi-magnify-plus-outline</v-icon>
</v-btn>
<v-btn @click.prevent.stop="zoomOut" title="Zoom out" :disabled="isMinZoom" icon>
<v-icon color="#000">mdi-magnify-minus-outline</v-icon>
</v-btn>
<slot name="toolbar"/>
</v-toolbar>
<div id="panzoom">
<slot/>
</div>
</div>
</template>
<script setup lang="ts">
import {computed, onMounted, ref, defineProps, defineExpose} from "vue";
import createPanZoom from "panzoom";
import * as panzoom from "panzoom";
const props = defineProps<{
config: panzoom.PanZoomOptions
}>();
const panZoom = ref<panzoom.PanZoom | null>(null);
const zoomLevel = ref<number>(1);
const minZoom = ref<number>(0.5);
const maxZoom = ref<number>(1.5);
const isMaxZoom = computed(() => {
if (zoomLevel.value) {
return zoomLevel.value >= maxZoom.value;
}
return false;
});
const isMinZoom = computed(() => {
if (zoomLevel.value) {
return zoomLevel.value <= minZoom.value;
}
return false;
});
const initialZoom = ref<number>(1);
onMounted(() => {
const canvas = document.getElementById("panzoom")
if (!canvas) {
throw new Error("No element with id panzoom")
}
const firstChild = canvas.firstElementChild
if (!firstChild) {
throw new Error("No child element")
}
const startX = canvas.getBoundingClientRect().width / 2 - firstChild.getBoundingClientRect().width / 2
const startY = canvas.getBoundingClientRect().height / 2 - firstChild.getBoundingClientRect().height / 2
initialZoom.value = props.config.initialZoom ? props.config.initialZoom : 1
minZoom.value = props.config.initialZoom / 2
maxZoom.value = props.config.initialZoom * 2
panZoom.value = createPanZoom(canvas, {
...props.config,
zoomDoubleClickSpeed: 1, // disable double click zoom
autocenter: true,
initialX: startX,
initialY: startY,
minZoom: minZoom.value,
maxZoom: maxZoom.value,
});
panZoom.value.on("zoom", (e: panzoom.PanZoom) => {
zoomLevel.value = e.getTransform().scale;
});
reset(false);
});
const ZOOM_FACTOR = 0.255
const zoomIn = () => {
if (!panZoom.value) return
const currentZoom = panZoom.value.getTransform().scale
panZoom.value.smoothZoomAbs(0, 0, currentZoom + ZOOM_FACTOR)
}
const zoomOut = () => {
if (!panZoom.value) return
const currentZoom = panZoom.value.getTransform().scale
panZoom.value.smoothZoomAbs(0, 0, currentZoom - ZOOM_FACTOR)
}
const reset = (smooth = true) => {
if (!panZoom.value) return
const canvas = document.getElementById("panzoom")
if (!canvas) {
throw new Error("No element with id panzoom")
}
const firstChild = canvas.firstElementChild
if (!firstChild) {
throw new Error("No child element")
}
const startX = canvas.getBoundingClientRect().width / 2 - firstChild.getBoundingClientRect().width / 2
const startY = canvas.getBoundingClientRect().height / 2 - firstChild.getBoundingClientRect().height / 2
panZoom.value.pause()
if (smooth) {
panZoom.value.smoothZoomAbs(0, 0, initialZoom.value)
panZoom.value.smoothMoveTo(startX * 0.5, startY)
} else {
panZoom.value.zoomTo(0, 0, initialZoom.value)
panZoom.value.moveTo(startX * 0.5, startY)
}
panZoom.value.resume()
}
defineExpose({
reset,
zoomIn,
zoomOut,
zoomLevel,
isMaxZoom,
isMinZoom,
});
</script>
<style>
#graphwrapper {
position: relative;
width: 100%;
height: 100%;
}
#gtb .v-toolbar__content {
padding: 0;
}
#panzoom {
width: 100%;
height: 100%;
position: relative;
cursor: move;
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<v-row style="position: relative; min-height: 600px">
<v-col :cols="(selectedStep && selectedStep in playbook.tasks) || showNewDialog ? 8 :12">
<v-card style="overflow: hidden" outlined>
<v-card-text>
<PanZoom ref="panZoomPanel" :config="panZoomConfig">
<template #default>
<PlaybookGraph
v-if="playbook"
:playbook="playbook"
:horizontal="horizontal"
:selected="selectedStep"
@update:selected="showNewDialog = false; selectedStep = $event"
/>
</template>
<template #actionbar>
<v-btn @click="showNewDialog = true; selectedStep = ''" large rounded>
<v-icon color="#000">mdi-plus</v-icon>
New Step
</v-btn>
</template>
<template #toolbar>
<v-btn @click="toggleOrientation" label="Toggle Orientation" icon>
<v-icon color="#000">mdi-format-rotate-90</v-icon>
</v-btn>
</template>
</PanZoom>
</v-card-text>
</v-card>
</v-col>
<v-col
v-if="(selectedStep && selectedStep in playbook.tasks) || showNewDialog"
cols="4"
>
<EditTask
v-if="selectedStep && selectedStep in playbook.tasks"
:value="playbook.tasks[selectedStep]"
@input="updateTask"
:possibleNexts="possibleNexts"
:parents="parents"
:playbook="playbook"
@delete="deleteTask"
@close="unselectStep"/>
<NewTask
v-else-if="showNewDialog"
:playbook="playbook"
@createTask="createTask"
@close="showNewDialog = false"/>
</v-col>
</v-row>
</template>
<script lang="ts" setup>
import {computed, nextTick, ref, defineProps, set, defineEmits, del} from "vue";
import PlaybookGraph from "@/components/playbookeditor/PlaybookGraph.vue";
import PanZoom from "@/components/playbookeditor/PanZoom.vue";
import EditTask from "@/components/playbookeditor/EditTask.vue";
import NewTask from "@/components/playbookeditor/NewTask.vue";
const props = defineProps({
'value': {
type: Object,
required: true
}
});
const emit = defineEmits(['input']);
const playbook = computed({
get: () => props.value,
set: (value) => {
emit('input', value);
}
});
// selected step
const selectedStep = ref("")
const unselectStep = () => {
selectedStep.value = "";
}
const updateTask = (task: any) => {
set(playbook.value.tasks, selectedStep.value, task);
emit('input', playbook.value);
}
const deleteTask = () => {
const parents = Array<any>();
for (const task in playbook.value.tasks) {
if (playbook.value.tasks[task].next && playbook.value.tasks[task].next[selectedStep.value]) {
parents.push(task);
}
}
const children = Array<any>();
if (playbook.value.tasks[selectedStep.value].next) {
for (const next in playbook.value.tasks[selectedStep.value].next) {
children.push(next);
}
}
for (const parent of parents) {
del(playbook.value.tasks[parent].next, selectedStep.value);
for (const child of children) {
set(playbook.value.tasks[parent].next, child, playbook.value.tasks[selectedStep.value].next[child]);
}
}
del(playbook.value.tasks, selectedStep.value);
// for (const task in playbook.value.tasks) {
// if (playbook.value.tasks[task].next && playbook.value.tasks[task].next[selectedStep.value]) {
// del(playbook.value.tasks[task].next, selectedStep.value);
// }
// }
// del(playbook.value.tasks, selectedStep.value);
emit('input', playbook.value);
}
const panZoomConfig = ref({})
const panZoomPanel = ref(null);
const horizontal = ref(false);
const toggleOrientation = () => {
horizontal.value = !horizontal.value;
nextTick(() => {
panZoomPanel.value?.reset(false);
});
}
const showNewDialog = ref(false);
const createTask = (task: any) => {
const t = {
name: task.name,
description: task.description,
type: 'task',
next: {}
};
set(playbook.value.tasks, task.key, t);
selectedStep.value = task.key;
};
// edit task
const possibleNexts = computed(() => {
if (!selectedStep.value) {
return [];
}
let nexts = Object.keys(playbook.value.tasks);
nexts = nexts.filter((n) => n !== selectedStep.value);
// remove any nexts that are already in the list
if (playbook.value.tasks[selectedStep.value] && 'next' in playbook.value.tasks[selectedStep.value]) {
for (const next in playbook.value.tasks[selectedStep.value].next) {
nexts = nexts.filter((n) => n !== next);
}
}
// remove parents recursively
const parents = findAncestor(selectedStep.value);
for (const parent of parents) {
nexts = nexts.filter((n) => n !== parent);
}
const result: Array<Record<string, string>> = [];
for (const next of nexts) {
if (next && playbook.value.tasks[next].name) {
result.push({"key": next, "name": playbook.value.tasks[next].name});
}
}
return result;
});
const findAncestor = (step: string): Array<string> => {
const parents: Array<string> = [];
for (const task in playbook.value.tasks) {
for (const next in playbook.value.tasks[task].next) {
if (next === step) {
if (!parents.includes(task)) {
parents.push(task);
parents.push(...findAncestor(task));
}
}
}
}
return parents;
};
const parents = computed(() => {
const parents: Array<string> = [];
for (const task in playbook.value.tasks) {
for (const next in playbook.value.tasks[task].next) {
if (next === selectedStep.value) {
if (!parents.includes(task)) {
parents.push(task);
}
}
}
}
return parents;
});
</script>
<style>
#graphwrapper {
padding: 20px;
}
#gab {
position: absolute;
left: 10px;
top: 20px;
border-radius: 30px;
z-index: 200;
}
#gtb {
position: absolute;
left: 0;
bottom: 20px;
border-radius: 30px;
z-index: 200;
}
#gtb .v-toolbar__content > .v-btn:first-child {
margin-inline-start: 0;
}
#gtb .v-toolbar__content > .v-btn:last-child {
margin-inline-end: 0;
}
</style>

View File

@@ -0,0 +1,511 @@
<template>
<svg
class="pe-graph"
:width="width * scale"
:height="height * scale"
:viewBox="`${minX - config.graphPadding} ${minY - config.graphPadding} ${maxX - minX + config.boxWidth + config.graphPadding * 2} ${maxY - minY + config.boxHeight + config.graphPadding * 2}`"
>
<defs>
<path
id="cog"
d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/>
<path
id="clipboard-outline"
d="M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M7,7H17V5H19V19H5V5H7V7Z"/>
<path
id="keyboard"
d="M19,10H17V8H19M19,13H17V11H19M16,10H14V8H16M16,13H14V11H16M16,17H8V15H16M7,10H5V8H7M7,13H5V11H7M8,11H10V13H8M8,8H10V10H8M11,11H13V13H11M11,8H13V10H11M20,5H4C2.89,5 2,5.89 2,7V17A2,2 0 0,0 4,19H20A2,2 0 0,0 22,17V7C22,5.89 21.1,5 20,5Z"/>
<path
id="star"
d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
</defs>
<g class="pe-links">
<path v-for="(link, index) in links" :key="index" :d="link.path"/>
</g>
<g class="pe-activelinks">
<path
v-for="(link, index) in links"
:key="index"
:d="link.path"
:class="{
'hovered': !selectedNode && (link.source === hoverNode || link.target === hoverNode),
'selected': link.source === selectedNode || link.target === selectedNode
}"
/>
</g>
<g class="pe-nodes">
<g
v-for="(node, index) in positionedNodes"
:key="index">
<g
v-if="node.type === 'start'"
:transform="`translate(${props.horizontal ? node.x + config.boxWidth : node.x + (config.boxWidth / 2)}, ${node.y})`"
class="start"
>
<circle
:transform="`translate(0, ${config.boxHeight / 2})`"
r="20"
/>
<g
class="icon"
:transform="`translate(${0 - 12}, ${(config.boxHeight - 24) / 2})`"
>
<use href="#star" fill="white"/>
</g>
</g>
<g
v-else
:transform="`translate(${node.x}, ${node.y})`"
:class="{
'pe-node': true,
'start': node.type === 'start',
'hovered': node.id === hoverNode,
'selected': node.id === selectedNode,
'unhovered': hoverNode && !selectedNode && node.id !== hoverNode,
'unselected': selectedNode && node.id !== selectedNode,
}"
@mouseover="hoverNode = node.id"
@mouseout="hoverNode = null"
@click="selectedNode === node.id ? selectedNode = null : selectedNode = node.id"
>
<rect
class="pe-box"
:width="config.boxWidth"
:height="config.boxHeight"
:rx="config.boxRadius"
:ry="config.boxRadius"
/>
<g
class="pe-icon"
:transform="`translate(10, ${(config.boxHeight - 24) / 2})`"
>
<use v-if="node.type === 'automation'" href="#cog"/>
<use v-else-if="node.type === 'start'" href="#star"/>
<use v-else-if="node.type === 'input'" href="#keyboard"/>
<use v-else href="#clipboard-outline"/>
</g>
<text
class="pe-text"
:x="config.boxWidth / 2"
:y="config.boxHeight / 2"
>
{{ node.label ? node.label : node.id }}
</text>
<g
class="add"
:transform="`translate(${config.boxWidth - 34}, ${(config.boxHeight - 24) / 2})`"
>
<use href="#star" fill="none"/>
</g>
</g>
</g>
</g>
<g class="pe-connectors">
<g
v-for="(link, index) in links"
:key="index"
:class="{
'pe-connector': true,
'hovered': !selectedNode && (link.source === hoverNode || link.target === hoverNode),
'selected': link.source === selectedNode || link.target === selectedNode
}"
>
<circle
v-if="link.source !== 'start'"
:cx="link.start.x"
:cy="link.start.y"
:r="4"/>
<circle
:cx="link.end.x"
:cy="link.end.y"
:r="7"/>
<circle
:cx="link.end.x"
:cy="link.end.y"
:r="5"
fill="white"/>
</g>
</g>
</svg>
</template>
<script setup lang="ts">
import * as d3 from "d3";
import {digl} from "@crinkles/digl";
import {Edge as DiglEdge, Node as DiglNode, Position, Rank} from "@crinkles/digl/dist/types";
import {computed, defineEmits, ref, Ref, defineProps, ComputedRef} from "vue";
interface Config {
graphPadding: number;
boxWidth: number;
boxHeight: number;
boxMarginX: number;
boxMarginY: number;
boxRadius: number;
lineDistance: number;
}
const props = defineProps({
playbook: {
type: Object,
required: true
},
selected: {
type: String,
required: false,
default: null
},
horizontal: {
type: Boolean,
required: false,
default: false
},
scale: {
type: Number,
required: false,
default: 1
}
})
const config = ref({
graphPadding: 10,
boxWidth: 220,
boxHeight: 40,
boxMarginX: 60,
boxMarginY: 70,
boxRadius: 20,
lineDistance: 0,
});
interface Edge extends DiglEdge {
label?: { text: string, x: number, y: number };
}
interface Node extends DiglNode {
type: 'automation' | 'input' | 'task' | 'start';
}
const edges = computed(() => {
const edges: Array<Edge> = [];
for (const key in props.playbook.tasks) {
for (const next in props.playbook.tasks[key].next) {
edges.push({
source: key,
target: next,
label: props.playbook.tasks[key].next[next]
});
}
}
const rootNodes = nodes.value.filter(node => edges.every(edge => edge.target !== node.id));
for (const node of rootNodes) {
if (node.id !== 'start') {
edges.push({
source: 'start',
target: node.id
});
}
}
return edges;
});
const nodes: ComputedRef<Array<Node>> = computed(() => {
const nodes = [{
id: "start",
label: "Start",
type: "start" as 'automation' | 'input' | 'task' | 'start',
}];
for (const key in props.playbook.tasks) {
nodes.push({
id: key,
label: props.playbook.tasks[key].name,
type: props.playbook.tasks[key].type
});
}
return nodes;
});
const hoverNode: Ref<string | null> = ref(null);
const emits = defineEmits(['update:selected']);
const selectedNode = computed({
get: () => props.selected,
set: (newVal) => {
emits('update:selected', newVal);
}
});
const rankses: Ref<Array<Array<Rank>>> = computed(() => {
return digl(edges.value);
});
const ranks = computed(() => {
return rankses.value.length > 0 ? rankses.value[0] : [];
});
const positionedNodes = computed(() => {
return positioning(config.value, props.horizontal, nodes.value, ranks.value);
});
const width = computed(() => {
return maxX.value - minX.value + config.value.graphPadding * 2 + config.value.boxWidth;
});
const height = computed(() => {
return maxY.value - minY.value + config.value.graphPadding * 2 + config.value.boxHeight;
});
const minX = computed(() => {
if (props.horizontal) {
return Math.min(...positionedNodes.value.map(node => node.x)) + config.value.boxWidth - 22;
}
return Math.min(...positionedNodes.value.map(node => node.x));
});
const minY = computed(() => {
if (!props.horizontal) {
return Math.min(...positionedNodes.value.map(node => node.y)) + config.value.boxHeight - 30;
}
return Math.min(...positionedNodes.value.map(node => node.y));
});
const maxX = computed(() => {
return Math.max(...positionedNodes.value.map(node => node.x));
});
const maxY = computed(() => {
return Math.max(...positionedNodes.value.map(node => node.y)) + 50;
});
const links = computed(() => {
return edges.value.map(edge => {
const source = positionedNodes.value.find(node => node.id === edge.source);
const target = positionedNodes.value.find(node => node.id === edge.target);
if (!source || !target) return;
// index within rank
const sourceIndex = ranks.value.find(rank => rank.includes(edge.source))?.indexOf(edge.source);
if (sourceIndex === undefined) {
throw new Error(`sourceIndex is undefined for ${edge.source}`);
}
const path = props.horizontal ?
horizontalConnectionLine(source, target, sourceIndex, config.value) :
verticalConnectionLine(source, target, sourceIndex, config.value);
const start = props.horizontal ?
{x: source.x + config.value.boxWidth, y: source.y + config.value.boxHeight / 2} :
{x: source.x + config.value.boxWidth / 2, y: source.y + config.value.boxHeight};
const end = props.horizontal ?
{x: target.x, y: target.y + config.value.boxHeight / 2} :
{x: target.x + config.value.boxWidth / 2, y: target.y};
return {
source: edge.source,
target: edge.target,
path: path.toString(),
start: start,
end: end,
};
}).filter(link => link);
});
interface PositionedNode extends Node, Position {
}
function positioning(
config: Config,
horizontal: boolean,
nodes: Node[],
ranks: Rank[]
): PositionedNode[] {
const _nodes: PositionedNode[] = [];
const _h = horizontal;
ranks.forEach((rank, i) => {
const xStart = _h
? (config.boxWidth + config.boxMarginX) * i
: -0.5 * (rank.length - 1) * (config.boxWidth + config.boxMarginX);
const yStart = _h
? -0.5 * (rank.length - 1) * (config.boxHeight + config.boxMarginY)
: (config.boxHeight + config.boxMarginY) * i;
rank.forEach((nodeId, nIndex) => {
const _node: Node = nodes.find((n) => n.id == nodeId) as Node;
if (!_node) return;
const x = _h ? xStart : xStart + (config.boxWidth + config.boxMarginX) * nIndex;
const y = _h ? yStart + (config.boxHeight + config.boxMarginY) * nIndex : yStart;
_nodes.push({..._node, x, y});
});
});
return _nodes;
}
function verticalConnectionLine(source: PositionedNode, target: PositionedNode, sourceIndex: number, config: Config) {
const sourceBottomCenter = {
x: source.x + config.boxWidth / 2,
y: source.y + config.boxHeight,
};
const targetTopCenter = {
x: target.x + config.boxWidth / 2,
y: target.y,
};
const path = d3.path();
path.moveTo(sourceBottomCenter.x, sourceBottomCenter.y);
const lineCurve = config.boxMarginY / 2;
if (sourceBottomCenter.x == targetTopCenter.x) {
path.lineTo(targetTopCenter.x, targetTopCenter.y);
} else if (sourceBottomCenter.x < targetTopCenter.x) {
if (target.y !== source.y + config.boxHeight + config.boxMarginY) {
path.lineTo(sourceBottomCenter.x, target.y - config.boxMarginY);
}
sourceBottomCenter.y = target.y - config.boxMarginY;
path.quadraticCurveTo(
sourceBottomCenter.x, sourceBottomCenter.y + lineCurve + sourceIndex * config.lineDistance,
sourceBottomCenter.x + lineCurve, sourceBottomCenter.y + lineCurve + sourceIndex * config.lineDistance,
);
path.lineTo(targetTopCenter.x - lineCurve, targetTopCenter.y - lineCurve + sourceIndex * config.lineDistance);
path.quadraticCurveTo(
targetTopCenter.x, targetTopCenter.y - lineCurve + sourceIndex * config.lineDistance,
targetTopCenter.x, targetTopCenter.y,
);
} else {
if (target.y !== source.y + config.boxHeight + config.boxMarginY) {
path.lineTo(sourceBottomCenter.x, target.y - config.boxMarginY);
}
sourceBottomCenter.y = target.y - config.boxMarginY;
path.quadraticCurveTo(
sourceBottomCenter.x, sourceBottomCenter.y + lineCurve + sourceIndex * config.lineDistance,
sourceBottomCenter.x - lineCurve, sourceBottomCenter.y + lineCurve + sourceIndex * config.lineDistance,
);
path.lineTo(targetTopCenter.x + lineCurve, targetTopCenter.y - lineCurve + sourceIndex * config.lineDistance);
path.quadraticCurveTo(
targetTopCenter.x, targetTopCenter.y - lineCurve + sourceIndex * config.lineDistance,
targetTopCenter.x, targetTopCenter.y,
);
}
return path;
}
function horizontalConnectionLine(source: PositionedNode, target: PositionedNode, sourceIndex: number, config: Config) {
const sourceRightCenter = {
x: source.x + config.boxWidth,
y: source.y + config.boxHeight / 2,
};
const targetLeftCenter = {
x: target.x,
y: target.y + config.boxHeight / 2,
};
const path = d3.path();
path.moveTo(sourceRightCenter.x, sourceRightCenter.y);
const lineCurve = config.boxMarginX / 2;
if (sourceRightCenter.y == targetLeftCenter.y) {
path.lineTo(targetLeftCenter.x, targetLeftCenter.y);
} else if (sourceRightCenter.y < targetLeftCenter.y) {
if (target.x !== source.x + config.boxWidth + config.boxMarginX) {
path.lineTo(target.x - config.boxMarginX, sourceRightCenter.y);
}
sourceRightCenter.x = target.x - config.boxMarginX;
path.quadraticCurveTo(
sourceRightCenter.x + lineCurve + sourceIndex * config.lineDistance, sourceRightCenter.y,
sourceRightCenter.x + lineCurve + sourceIndex * config.lineDistance, sourceRightCenter.y + lineCurve,
);
path.lineTo(targetLeftCenter.x - lineCurve + sourceIndex * config.lineDistance, targetLeftCenter.y - lineCurve);
path.quadraticCurveTo(
targetLeftCenter.x - lineCurve + sourceIndex * config.lineDistance, targetLeftCenter.y,
targetLeftCenter.x, targetLeftCenter.y,
);
} else {
if (target.x !== source.x + config.boxWidth + config.boxMarginX) {
path.lineTo(target.x - config.boxMarginX, sourceRightCenter.y);
}
sourceRightCenter.x = target.x - config.boxMarginX;
path.quadraticCurveTo(
sourceRightCenter.x + lineCurve + sourceIndex * config.lineDistance, sourceRightCenter.y,
sourceRightCenter.x + lineCurve + sourceIndex * config.lineDistance, sourceRightCenter.y - lineCurve,
);
path.lineTo(targetLeftCenter.x - lineCurve + sourceIndex * config.lineDistance, targetLeftCenter.y + lineCurve);
path.quadraticCurveTo(
targetLeftCenter.x - lineCurve + sourceIndex * config.lineDistance, targetLeftCenter.y,
targetLeftCenter.x, targetLeftCenter.y,
);
}
return path;
}
</script>
<style>
svg .pe-node.unhovered {
opacity: 0.5;
}
svg .pe-node .pe-box {
fill: white;
stroke: #4C566A;
stroke-width: 1px;
filter: drop-shadow(0 1.5px 1.5px rgba(0, 0, 0, .15));
}
svg .pe-node.selected .pe-box {
stroke: #88C0D0;
stroke-width: 2;
}
svg .pe-node text {
text-anchor: middle;
dominant-baseline: middle;
font-size: 16px;
}
svg .pe-node:hover .pe-box {
filter: drop-shadow(0 0 0.1rem #333);
}
svg .pe-node:hover .pe-box,
svg .pe-node:hover text {
cursor: pointer;
}
svg .pe-node:hover .add use {
/* fill: red !important; */
}
svg .pe-links path {
stroke: black;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, .15));
}
svg .pe-activelinks path {
stroke: none;
fill: none;
stroke-width: 2;
stroke-linejoin: round;
}
svg .pe-activelinks path.selected,
svg .pe-activelinks path.hovered {
stroke-width: 3;
stroke-linejoin: round;
stroke: #88C0D0;
filter: drop-shadow(0 0 0.1rem #88C0D0);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="fill-height">
<div class="network-graph fill-height">
<v-card
v-if="selected !== undefined"
class="mt-3 ml-3 px-0"
@@ -184,33 +184,33 @@ export default Vue.extend({
</script>
<style>
.node,
.node.selected {
.network-graph .node,
.network-graph .node.selected {
stroke: #388E3C !important;
}
.node.event {
.network-graph .node.event {
stroke: #D32F2F !important;
}
.node.center {
.network-graph .node.center {
stroke: #FFEB3B !important;
fill: #FFEB3B !important;
}
.theme--dark .node-label,
.theme--dark .node-label.event {
.theme--dark .network-graph .node-label,
.theme--dark .network-graph .node-label.event {
fill: #ffffff !important;
}
.node-label,
.node-label.event {
.network-graph .node-label,
.network-graph .node-label.event {
fill: #000000 !important;
}
.link {
.network-graph .link {
stroke: #424242 !important;
}
.link.selected,
.link:hover,.node:hover{
.network-graph .link.selected,
.network-graph .link:hover,.node:hover{
stroke: #FFEB3B !important;
}
</style>

View File

@@ -19,25 +19,35 @@
<v-alert v-else-if="error" color="warning">
{{ error }}
</v-alert>
<div v-else class="px-4 overflow-scroll">
<vue-pipeline
v-if="pipelineData"
ref="pipeline"
:x="50"
:y="55"
:data="pipelineData"
:showArrow="true"
:ystep="70"
:xstep="100"
lineStyle="default"
/>
</div>
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
Playbook
</v-subheader>
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
<Editor v-model="playbook.yaml" @input="updatePipeline" lang="yaml" :readonly="readonly"></Editor>
<v-tabs
v-model="tab"
background-color="transparent"
>
<v-tab>Graph (experimental)</v-tab>
<v-tab>YAML</v-tab>
</v-tabs>
<v-tabs-items v-model="tab" style="background: transparent">
<v-tab-item>
<v-text-field
v-model="playbook.name"
label="Name"
outlined
dense
:readonly="readonly"
class="mt-4"
/>
<PlaybookEditor
v-if="playbookJSON"
v-model="playbookJSON" />
</v-tab-item>
<v-tab-item>
<v-card class="py-2">
<Editor v-model="playbookYAML" lang="yaml" :readonly="readonly"></Editor>
</v-card>
</v-tab-item>
</v-tabs-items>
</div>
<v-row v-if="!readonly" class="px-3 my-6 flex-grow-0 flex-shrink-0">
@@ -62,6 +72,7 @@ import Editor from "../components/Editor.vue";
import {alg, Graph} from "graphlib";
import yaml from 'yaml';
import Ajv from "ajv";
import PlaybookEditor from "@/components/playbookeditor/PlaybookEditor.vue";
const playbookSchema = {
type: "object",
@@ -100,8 +111,10 @@ interface State {
playbook?: PlaybookTemplate;
g: Record<string, any>;
selected: any;
pipelineData: any;
error: string;
tab: number;
playbookYAML: string;
playbookJSON: any;
}
interface TaskWithID {
@@ -140,17 +153,26 @@ const inityaml = "name: VirusTotal hash check\n" +
export default Vue.extend({
name: "Playbook",
components: { Editor },
components: { Editor, PlaybookEditor },
data: (): State => ({
playbook: undefined,
g: {},
selected: undefined,
pipelineData: undefined,
error: "",
tab: 1,
playbookJSON: undefined,
playbookYAML: inityaml
}),
watch: {
'$route': function () {
this.loadPlaybook();
},
tab: function (value) {
if (value === 0) {
this.playbookJSON = yaml.parse(this.playbookYAML);
} else {
this.playbookYAML = yaml.stringify(this.playbookJSON);
}
}
},
computed: {
@@ -198,77 +220,26 @@ export default Vue.extend({
}
return tasks;
},
updatePipeline: function () {
if (this.playbook) {
this.pipeline(this.playbook.yaml);
}
},
pipeline: function(playbookYAML: string) {
try {
let playbook = yaml.parse(playbookYAML);
this.error = "";
let g = new Graph();
for (const stepKey in playbook.tasks) {
g.setNode(stepKey);
}
this.lodash.forEach(playbook.tasks, (task: Task, stepKey: string) => {
if ("next" in task) {
this.lodash.forEach(task.next, (condition, nextKey) => {
g.setEdge(stepKey, nextKey);
});
}
});
let tasks = this.tasks(g, playbook);
let elements = [] as Array<any>;
this.lodash.forEach(tasks, task => {
elements.push({
id: task.id,
name: task.task.name,
next: [],
status: "unknown"
});
});
this.lodash.forEach(tasks, (task: TaskWithID) => {
if ("next" in task.task) {
this.lodash.forEach(task.task.next, (condition, nextKey) => {
let nextID = this.lodash.findIndex(elements, ["id", nextKey]);
let stepID = this.lodash.findIndex(elements, ["id", task.id]);
if (nextID !== -1) {
// TODO: invalid schema
elements[stepID].next.push({index: nextID});
}
});
}
});
this.pipelineData = undefined;
this.$nextTick(() => {
this.pipelineData = this.lodash.values(elements);
})
}
catch (e: unknown) {
console.log(e);
this.error = this.lodash.toString(e);
}
},
save() {
if (this.playbook === undefined) {
return;
}
if (this.$route.params.id == 'new') {
let playbook = this.playbook;
if (this.tab === 0) {
let jsonData = this.playbookJSON;
jsonData["name"] = playbook.name;
playbook.yaml = yaml.stringify(jsonData);
} else {
playbook.yaml = this.playbookYAML;
}
if (this.$route.params.id == 'new') {
// playbook.id = kebabCase(playbook.name);
API.createPlaybook(playbook).then(() => {
this.$store.dispatch("alertSuccess", { name: "Playbook created" });
});
} else {
API.updatePlaybook(this.$route.params.id, this.playbook).then(() => {
API.updatePlaybook(this.$route.params.id, playbook).then(() => {
this.$store.dispatch("alertSuccess", { name: "Playbook saved" });
});
}
@@ -279,9 +250,13 @@ export default Vue.extend({
}
if (this.$route.params.id == 'new') {
this.playbook = { name: "MyPlaybook", yaml: inityaml }
this.playbookJSON = yaml.parse(this.playbook.yaml);
this.playbookYAML = this.playbook.yaml;
} else {
API.getPlaybook(this.$route.params.id).then((response) => {
this.playbook = response.data;
this.playbookJSON = yaml.parse(this.playbook.yaml);
this.playbookYAML = this.playbook.yaml;
});
}
},
@@ -290,7 +265,7 @@ export default Vue.extend({
return this.lodash.includes(this.$store.state.settings.roles, s);
}
return false;
}
},
},
mounted() {
this.loadPlaybook();

View File

@@ -295,19 +295,13 @@
<v-icon small>mdi-close-circle</v-icon>
</v-btn>
</v-card-subtitle>
<div style="overflow-x: scroll">
<vue-pipeline
v-if="showPipelines"
ref="pipeline"
:x="50"
:y="55"
:data="pipeline(playbook)"
:showArrow="true"
:ystep="70"
:xstep="90"
lineStyle="default"
@select="select"
class="mx-4"
<div style="overflow-x: scroll; text-align: center">
<PlaybookGraph
v-if="playbook"
:playbook="playbook"
horizontal
:scale="0.3"
style="margin: 0 auto"
/>
</div>
<v-list dense color="cards" class="tasks py-0">
@@ -319,7 +313,6 @@
link
@click="selectTask(taskwithid, playbookid)"
>
<!--template v-slot:activator-->
<v-list-item-icon>
<v-icon
:class="{
@@ -863,6 +856,8 @@ import {DateTime} from "luxon";
import VueMarkdown from "vue-markdown";
import JSONHTML from "../components/JSONHTML.vue";
import TicketNew from "@/views/TicketNew.vue";
import yaml from "yaml";
import PlaybookGraph from "@/components/playbookeditor/PlaybookGraph.vue";
interface State {
valid: boolean;
@@ -929,6 +924,7 @@ interface TaskWithID {
export default Vue.extend({
name: "Ticket",
components: {
PlaybookGraph,
TicketNew,
Dashboard,
ArtifactSnippet,
@@ -999,6 +995,9 @@ export default Vue.extend({
}
},
computed: {
yaml() {
return yaml
},
schema: function() {
if (this.ticket !== undefined && this.ticket.schema !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1002,6 +1002,11 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@crinkles/digl@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@crinkles/digl/-/digl-2.0.2.tgz#add3574124c29dbb3e262624d412d64e8b5d1b54"
integrity sha512-lwdBaMIcZrs0PGHsbua0wjbSgzWtYvIzBhaS7Xr4UZm+QAh727IQqoOVk12EYT41t6XfdZVdkwYN6aeYA3q4jw==
"@cypress/request@^2.88.10":
version "2.88.10"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
@@ -2851,6 +2856,13 @@ alphanum-sort@^1.0.0:
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==
amator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/amator/-/amator-1.1.0.tgz#08c6b60bc93aec2b61bbfc0c4d677d30323cc0f1"
integrity sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==
dependencies:
bezier-easing "^2.0.3"
ansi-colors@^3.0.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@@ -3444,6 +3456,11 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
bezier-easing@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
integrity sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==
bfj@^6.1.1:
version "6.1.2"
resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.2.tgz#325c861a822bcb358a41c78a33b8e6e2086dde7f"
@@ -4267,6 +4284,11 @@ commander@2.17.x:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commander@7:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^2.12.1, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.9.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -4879,16 +4901,108 @@ cypress@11.2.0:
untildify "^4.0.0"
yauzl "^2.10.0"
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.1.tgz#39331ea706f5709417d31bbb6ec152e0328b39b3"
integrity sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==
dependencies:
internmap "1 - 2"
d3-axis@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
d3-brush@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
dependencies:
d3-dispatch "1 - 3"
d3-drag "2 - 3"
d3-interpolate "1 - 3"
d3-selection "3"
d3-transition "3"
d3-chord@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
dependencies:
d3-path "1 - 3"
d3-collection@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
"d3-color@1 - 3", d3-color@3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
d3-contour@4:
version "4.0.0"
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.0.tgz#5a1337c6da0d528479acdb5db54bc81a0ff2ec6b"
integrity sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==
dependencies:
d3-array "^3.2.0"
d3-delaunay@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92"
integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==
dependencies:
delaunator "5"
d3-dispatch@1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
"d3-dispatch@1 - 3", d3-dispatch@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
"d3-drag@2 - 3", d3-drag@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
dependencies:
d3-dispatch "1 - 3"
d3-selection "3"
"d3-dsv@1 - 3", d3-dsv@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
dependencies:
commander "7"
iconv-lite "0.6"
rw "1"
"d3-ease@1 - 3", d3-ease@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
d3-fetch@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
dependencies:
d3-dsv "1 - 3"
d3-force@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
dependencies:
d3-dispatch "1 - 3"
d3-quadtree "1 - 3"
d3-timer "1 - 3"
d3-force@^1.0.6:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b"
@@ -4899,16 +5013,168 @@ d3-force@^1.0.6:
d3-quadtree "1"
d3-timer "1"
"d3-format@1 - 3", d3-format@3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
d3-geo@3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
dependencies:
d3-array "2.5.0 - 3"
d3-hierarchy@3:
version "3.1.2"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-polygon@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
d3-quadtree@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135"
integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==
"d3-quadtree@1 - 3", d3-quadtree@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
d3-random@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
d3-scale-chromatic@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
dependencies:
d3-color "1 - 3"
d3-interpolate "1 - 3"
d3-scale@4:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
"d3-selection@2 - 3", d3-selection@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
d3-shape@3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4", d3-time-format@4:
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
d3-timer@1:
version "1.0.10"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
"d3-timer@1 - 3", d3-timer@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
"d3-transition@2 - 3", d3-transition@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
dependencies:
d3-color "1 - 3"
d3-dispatch "1 - 3"
d3-ease "1 - 3"
d3-interpolate "1 - 3"
d3-timer "1 - 3"
d3-zoom@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
dependencies:
d3-dispatch "1 - 3"
d3-drag "2 - 3"
d3-interpolate "1 - 3"
d3-selection "2 - 3"
d3-transition "2 - 3"
d3@^7.8.0:
version "7.8.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.0.tgz#c9441f0ea9266b1003a97c2ffd53e79e9e14b1fc"
integrity sha512-a5rNemRadWkEfqnY5NsD4RdCP9vn8EIJ4I5Rl14U0uKH1SXqcNmk/h9aGaAF1O98lz6L9M0IeUcuPa9GUYbI5A==
dependencies:
d3-array "3"
d3-axis "3"
d3-brush "3"
d3-chord "3"
d3-color "3"
d3-contour "4"
d3-delaunay "6"
d3-dispatch "3"
d3-drag "3"
d3-dsv "3"
d3-ease "3"
d3-fetch "3"
d3-force "3"
d3-format "3"
d3-geo "3"
d3-hierarchy "3"
d3-interpolate "3"
d3-path "3"
d3-polygon "3"
d3-quadtree "3"
d3-random "3"
d3-scale "4"
d3-scale-chromatic "3"
d3-selection "3"
d3-shape "3"
d3-time "3"
d3-time-format "4"
d3-timer "3"
d3-transition "3"
d3-zoom "3"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -5088,6 +5354,13 @@ del@^4.1.1:
pify "^4.0.1"
rimraf "^2.6.3"
delaunator@5:
version "5.0.0"
resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
dependencies:
robust-predicates "^3.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -7041,7 +7314,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.6.3:
iconv-lite@0.6, iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -7222,6 +7495,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
interpret@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
@@ -9424,6 +9702,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
ngraph.events@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/ngraph.events/-/ngraph.events-1.2.2.tgz#3ceb92d676a04a4e7ce60a09fa8e17a4f0346d7f"
integrity sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -9895,6 +10178,15 @@ pako@~1.0.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
panzoom@^9.4.3:
version "9.4.3"
resolved "https://registry.yarnpkg.com/panzoom/-/panzoom-9.4.3.tgz#195c4031bb643f2e6c42f1de0ca87cc10e224042"
integrity sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==
dependencies:
amator "^1.1.0"
ngraph.events "^1.2.2"
wheel "^1.0.0"
parallel-transform@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
@@ -11364,6 +11656,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
robust-predicates@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -11388,6 +11685,11 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
rw@1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
rxjs@^6.6.0:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
@@ -13540,6 +13842,11 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
wheel@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wheel/-/wheel-1.0.0.tgz#6cf46e06a854181adb8649228077f8b0d5c574ce"
integrity sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"