mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-10 17:23:05 +01:00
Add playbook editor (#702)
This commit is contained in:
@@ -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",
|
||||
|
||||
273
ui/src/components/playbookeditor/EditTask.vue
Normal file
273
ui/src/components/playbookeditor/EditTask.vue
Normal 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>
|
||||
80
ui/src/components/playbookeditor/NewTask.vue
Normal file
80
ui/src/components/playbookeditor/NewTask.vue
Normal 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>
|
||||
164
ui/src/components/playbookeditor/PanZoom.vue
Normal file
164
ui/src/components/playbookeditor/PanZoom.vue
Normal 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>
|
||||
235
ui/src/components/playbookeditor/PlaybookEditor.vue
Normal file
235
ui/src/components/playbookeditor/PlaybookEditor.vue
Normal 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>
|
||||
511
ui/src/components/playbookeditor/PlaybookGraph.vue
Normal file
511
ui/src/components/playbookeditor/PlaybookGraph.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
309
ui/yarn.lock
309
ui/yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user