mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-27 05:36:01 +00:00
solve overlaps alg layout
This commit is contained in:
@@ -55,6 +55,7 @@
|
|||||||
"uuid": "^3.4.0"
|
"uuid": "^3.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chartjs-plugin-zoom": "^1.2.0"
|
"chartjs-plugin-zoom": "^1.2.0",
|
||||||
|
"interval-operations": "^1.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,7 +523,16 @@
|
|||||||
|
|
||||||
graph.initialize();
|
graph.initialize();
|
||||||
|
|
||||||
const layout = GraphLayout.createCircle(graph, circleMiddle).springyAlg().doMoveSteps().fixViewBox();
|
const layout = GraphLayout
|
||||||
|
// initial circle layout
|
||||||
|
.createCircle(graph, circleMiddle)
|
||||||
|
// simulation with Hook's, Coulomb's and gravity law
|
||||||
|
.springyAlg()
|
||||||
|
// move nodes to avoid overlaps
|
||||||
|
.solveOverlaps()
|
||||||
|
// view box starts with [0,0]
|
||||||
|
.fixViewBox();
|
||||||
|
|
||||||
// layout.print();
|
// layout.print();
|
||||||
|
|
||||||
callChange(current => {
|
callChange(current => {
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { IBoxBounds, IPoint, rectangleDistance, rectangleIntersectArea, Vector2D } from './designerMath';
|
import {
|
||||||
|
IBoxBounds,
|
||||||
|
IPoint,
|
||||||
|
rectangleDistance,
|
||||||
|
rectangleIntersectArea,
|
||||||
|
solveOverlapsInIntervalArray,
|
||||||
|
Vector2D,
|
||||||
|
} from './designerMath';
|
||||||
|
import { union, intersection } from 'interval-operations';
|
||||||
|
|
||||||
const MIN_NODE_DISTANCE = 50;
|
const MIN_NODE_DISTANCE = 50;
|
||||||
const SPRING_LENGTH = 100;
|
const SPRING_LENGTH = 100;
|
||||||
const SPRINGY_STEPS = 50;
|
const SPRINGY_STEPS = 50;
|
||||||
const GRAVITY_X = 0.005;
|
const GRAVITY_X = 0.005;
|
||||||
const GRAVITY_Y = 0.01;
|
const GRAVITY_Y = 0.01;
|
||||||
const REPULSION = 500_000;
|
// const REPULSION = 500_000;
|
||||||
|
const REPULSION = 1000;
|
||||||
const MAX_FORCE_SIZE = 100;
|
const MAX_FORCE_SIZE = 100;
|
||||||
const NODE_MARGIN = 20;
|
const NODE_MARGIN = 30;
|
||||||
const MOVE_STEP = 20;
|
|
||||||
const MOVE_BIG_STEP = 50;
|
// const MOVE_STEP = 20;
|
||||||
const MOVE_STEP_COUNT = 100;
|
// const MOVE_BIG_STEP = 50;
|
||||||
const MINIMAL_SCORE_BENEFIT = 1;
|
// const MOVE_STEP_COUNT = 100;
|
||||||
const SCORE_ASPECT_RATIO = 1.6;
|
// const MINIMAL_SCORE_BENEFIT = 1;
|
||||||
|
// const SCORE_ASPECT_RATIO = 1.6;
|
||||||
|
|
||||||
class GraphNode {
|
class GraphNode {
|
||||||
neightboors: GraphNode[] = [];
|
neightboors: GraphNode[] = [];
|
||||||
@@ -106,7 +116,10 @@ class LayoutNode {
|
|||||||
right: number;
|
right: number;
|
||||||
top: number;
|
top: number;
|
||||||
bottom: number;
|
bottom: number;
|
||||||
paddedRect: IBoxBounds;
|
// paddedRect: IBoxBounds;
|
||||||
|
|
||||||
|
rangeXPadded: [number, number];
|
||||||
|
rangeYPadded: [number, number];
|
||||||
|
|
||||||
constructor(public node: GraphNode, public x: number, public y: number) {
|
constructor(public node: GraphNode, public x: number, public y: number) {
|
||||||
this.left = x - node.width / 2;
|
this.left = x - node.width / 2;
|
||||||
@@ -115,12 +128,14 @@ class LayoutNode {
|
|||||||
this.bottom = y + node.height / 2;
|
this.bottom = y + node.height / 2;
|
||||||
this.position = new Vector2D(x, y);
|
this.position = new Vector2D(x, y);
|
||||||
|
|
||||||
this.paddedRect = {
|
this.rangeXPadded = [this.left - NODE_MARGIN, this.right + NODE_MARGIN];
|
||||||
left: this.left - NODE_MARGIN,
|
this.rangeYPadded = [this.top - NODE_MARGIN, this.bottom + NODE_MARGIN];
|
||||||
top: this.top - NODE_MARGIN,
|
// this.paddedRect = {
|
||||||
right: this.right + NODE_MARGIN,
|
// left: this.left - NODE_MARGIN,
|
||||||
bottom: this.bottom + NODE_MARGIN,
|
// top: this.top - NODE_MARGIN,
|
||||||
};
|
// right: this.right + NODE_MARGIN,
|
||||||
|
// bottom: this.bottom + NODE_MARGIN,
|
||||||
|
// };
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(dx: number, dy: number, forceMoveFixed = false) {
|
translate(dx: number, dy: number, forceMoveFixed = false) {
|
||||||
@@ -132,8 +147,14 @@ class LayoutNode {
|
|||||||
return rectangleDistance(this, node);
|
return rectangleDistance(this, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
intersectArea(node: LayoutNode) {
|
// intersectArea(node: LayoutNode) {
|
||||||
return rectangleIntersectArea(this.paddedRect, node.paddedRect);
|
// return rectangleIntersectArea(this.paddedRect, node.paddedRect);
|
||||||
|
// }
|
||||||
|
hasPaddedIntersect(node: LayoutNode) {
|
||||||
|
return !!(
|
||||||
|
intersection(this.rangeXPadded, node.rangeXPadded) &&
|
||||||
|
intersection(this.rangeYPadded, node.rangeYPadded)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,81 +330,113 @@ export class GraphLayout {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
score() {
|
// score() {
|
||||||
let res = 0;
|
// let res = 0;
|
||||||
|
|
||||||
for (const n1 of _.values(this.nodes)) {
|
// for (const n1 of _.values(this.nodes)) {
|
||||||
for (const n2 of _.values(this.nodes)) {
|
// for (const n2 of _.values(this.nodes)) {
|
||||||
if (n1.node.designerId == n2.node.designerId) {
|
// if (n1.node.designerId == n2.node.designerId) {
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
res += n1.intersectArea(n2);
|
// res += n1.intersectArea(n2);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const minX = _.min(_.values(this.nodes).map(n => n.left));
|
||||||
|
// const minY = _.min(_.values(this.nodes).map(n => n.top));
|
||||||
|
// const maxX = _.max(_.values(this.nodes).map(n => n.right));
|
||||||
|
// const maxY = _.max(_.values(this.nodes).map(n => n.bottom));
|
||||||
|
|
||||||
|
// res += maxX - minX;
|
||||||
|
// res += (maxY - minY) * SCORE_ASPECT_RATIO;
|
||||||
|
|
||||||
|
// return res;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tryMoveNode(node: LayoutNode): GraphLayout[] {
|
||||||
|
// if (node.node.fixedPosition) return [];
|
||||||
|
// return [
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(MOVE_STEP, 0) : x), false),
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(-MOVE_STEP, 0) : x), false),
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(0, MOVE_STEP) : x), false),
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(0, -MOVE_STEP) : x), false),
|
||||||
|
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
|
||||||
|
// this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tryMoveElement() {
|
||||||
|
// let res = null;
|
||||||
|
// let resScore = null;
|
||||||
|
|
||||||
|
// for (const node of _.values(this.nodes)) {
|
||||||
|
// for (const item of this.tryMoveNode(node)) {
|
||||||
|
// const score = item.score();
|
||||||
|
// if (resScore == null || score < resScore) {
|
||||||
|
// res = item;
|
||||||
|
// resScore = score;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return res;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// doMoveSteps() {
|
||||||
|
// let res: GraphLayout = this;
|
||||||
|
// let score = res.score();
|
||||||
|
// const start = new Date().getTime();
|
||||||
|
// for (let step = 0; step < MOVE_STEP_COUNT; step++) {
|
||||||
|
// const lastRes = res;
|
||||||
|
// res = res.tryMoveElement();
|
||||||
|
// if (!res) {
|
||||||
|
// lastRes.fillEdges();
|
||||||
|
// return lastRes;
|
||||||
|
// }
|
||||||
|
// const newScore = res.score();
|
||||||
|
// // console.log('STEP, SCORE, NEW SCORE', step, score, newScore);
|
||||||
|
// if (score - newScore < MINIMAL_SCORE_BENEFIT || new Date().getTime() - start > 1000) {
|
||||||
|
// lastRes.fillEdges();
|
||||||
|
// return lastRes;
|
||||||
|
// }
|
||||||
|
// score = newScore;
|
||||||
|
// }
|
||||||
|
// res.fillEdges();
|
||||||
|
// return res;
|
||||||
|
// }
|
||||||
|
|
||||||
|
solveOverlaps(): GraphLayout {
|
||||||
|
const nodes = _.sortBy(_.values(this.nodes), x => x.position.magnitude());
|
||||||
|
const res = new GraphLayout(this.graph);
|
||||||
|
for (const node of nodes) {
|
||||||
|
const placedNodes = _.values(res.nodes);
|
||||||
|
if (placedNodes.find(x => x.hasPaddedIntersect(node))) {
|
||||||
|
// intersection found, must perform moving algorithm
|
||||||
|
const xIntervalArray = union(
|
||||||
|
...placedNodes
|
||||||
|
.filter(x => intersection(x.rangeYPadded, node.rangeYPadded))
|
||||||
|
.map(x => x.rangeXPadded)
|
||||||
|
);
|
||||||
|
|
||||||
|
const yIntervalArray = union(
|
||||||
|
...placedNodes
|
||||||
|
.filter(x => intersection(x.rangeXPadded, node.rangeXPadded))
|
||||||
|
.map(x => x.rangeYPadded)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newX = solveOverlapsInIntervalArray(node.x, node.node.width, xIntervalArray as any);
|
||||||
|
const newY = solveOverlapsInIntervalArray(node.y, node.node.height, yIntervalArray as any);
|
||||||
|
|
||||||
|
if (newX < newY) res.nodes[node.node.designerId] = new LayoutNode(node.node, newX, node.y);
|
||||||
|
else res.nodes[node.node.designerId] = new LayoutNode(node.node, node.x, newY);
|
||||||
|
} else {
|
||||||
|
res.nodes[node.node.designerId] = node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const minX = _.min(_.values(this.nodes).map(n => n.left));
|
|
||||||
const minY = _.min(_.values(this.nodes).map(n => n.top));
|
|
||||||
const maxX = _.max(_.values(this.nodes).map(n => n.right));
|
|
||||||
const maxY = _.max(_.values(this.nodes).map(n => n.bottom));
|
|
||||||
|
|
||||||
res += maxX - minX;
|
|
||||||
res += (maxY - minY) * SCORE_ASPECT_RATIO;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
tryMoveNode(node: LayoutNode): GraphLayout[] {
|
|
||||||
if (node.node.fixedPosition) return [];
|
|
||||||
return [
|
|
||||||
this.changePositions(x => (x == node ? node.translate(MOVE_STEP, 0) : x), false),
|
|
||||||
this.changePositions(x => (x == node ? node.translate(-MOVE_STEP, 0) : x), false),
|
|
||||||
this.changePositions(x => (x == node ? node.translate(0, MOVE_STEP) : x), false),
|
|
||||||
this.changePositions(x => (x == node ? node.translate(0, -MOVE_STEP) : x), false),
|
|
||||||
|
|
||||||
this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
|
|
||||||
this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
|
|
||||||
this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
|
|
||||||
this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
tryMoveElement() {
|
|
||||||
let res = null;
|
|
||||||
let resScore = null;
|
|
||||||
|
|
||||||
for (const node of _.values(this.nodes)) {
|
|
||||||
for (const item of this.tryMoveNode(node)) {
|
|
||||||
const score = item.score();
|
|
||||||
if (resScore == null || score < resScore) {
|
|
||||||
res = item;
|
|
||||||
resScore = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
doMoveSteps() {
|
|
||||||
let res: GraphLayout = this;
|
|
||||||
let score = res.score();
|
|
||||||
const start = new Date().getTime();
|
|
||||||
for (let step = 0; step < MOVE_STEP_COUNT; step++) {
|
|
||||||
const lastRes = res;
|
|
||||||
res = res.tryMoveElement();
|
|
||||||
if (!res) {
|
|
||||||
lastRes.fillEdges();
|
|
||||||
return lastRes;
|
|
||||||
}
|
|
||||||
const newScore = res.score();
|
|
||||||
// console.log('STEP, SCORE, NEW SCORE', step, score, newScore);
|
|
||||||
if (score - newScore < MINIMAL_SCORE_BENEFIT || new Date().getTime() - start > 1000) {
|
|
||||||
lastRes.fillEdges();
|
|
||||||
return lastRes;
|
|
||||||
}
|
|
||||||
score = newScore;
|
|
||||||
}
|
|
||||||
res.fillEdges();
|
res.fillEdges();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { arrayDifference } from 'interval-operations';
|
||||||
|
import _ from 'lodash';
|
||||||
export interface IPoint {
|
export interface IPoint {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -134,3 +136,27 @@ export class Vector2D {
|
|||||||
return this.divide(this.magnitude());
|
return this.divide(this.magnitude());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function solveOverlapsInIntervalArray(position: number, size: number, usedIntervals: [number, number][]) {
|
||||||
|
const freeIntervals = arrayDifference([[-Infinity, Infinity]], usedIntervals) as [number, number][];
|
||||||
|
|
||||||
|
const candidates = [];
|
||||||
|
|
||||||
|
for (const interval of freeIntervals) {
|
||||||
|
const intervalSize = interval[1] - interval[0];
|
||||||
|
if (intervalSize < size) continue;
|
||||||
|
if (interval[1] < position) {
|
||||||
|
candidates.push(interval[1] - size / 2);
|
||||||
|
} else if (interval[0] > position) {
|
||||||
|
candidates.push(interval[0] - size / 2);
|
||||||
|
} else {
|
||||||
|
// position is in interval
|
||||||
|
let candidate = position;
|
||||||
|
if (candidate - size / 2 < interval[0]) candidate = interval[0] + size / 2;
|
||||||
|
if (candidate + size / 2 > interval[1]) candidate = interval[1] - size / 2;
|
||||||
|
candidates.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.minBy(candidates, x => Math.abs(x - position));
|
||||||
|
}
|
||||||
|
|||||||
28
packages/web/src/utility/statusBarStore.ts
Normal file
28
packages/web/src/utility/statusBarStore.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
export const statusBarTabInfo = writable({});
|
||||||
|
|
||||||
|
// export function updateStatuBarInfo(tabid, info) {
|
||||||
|
// statusBarTabInfo.update(x => ({
|
||||||
|
// ...x,
|
||||||
|
// [tabid]: info,
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function updateStatuBarInfoItem(tabid, key, item) {
|
||||||
|
statusBarTabInfo.update(tabs => {
|
||||||
|
const items = tabs[tabid] || [];
|
||||||
|
let newItems;
|
||||||
|
if (item == null) {
|
||||||
|
newItems = items.filter(x => x.key != key);
|
||||||
|
} else if (items.find(x => x.key == key)) {
|
||||||
|
newItems = items.map(x => (x.key == key ? { ...item, key } : x));
|
||||||
|
} else {
|
||||||
|
newItems = [...items, { ...item, key }];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...tabs,
|
||||||
|
[tabid]: newItems,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { getContext, onDestroy } from 'svelte';
|
import { getContext, onDestroy } from 'svelte';
|
||||||
import { updateStatuBarInfoItem } from '../widgets/StatusBar.svelte';
|
import { updateStatuBarInfoItem } from './statusBarStore';
|
||||||
|
|
||||||
function formatSeconds(duration) {
|
function formatSeconds(duration) {
|
||||||
if (duration == null) return '';
|
if (duration == null) return '';
|
||||||
|
|||||||
@@ -1,35 +1,5 @@
|
|||||||
<script lang="ts" context="module">
|
|
||||||
const statusBarTabInfo = writable({});
|
|
||||||
|
|
||||||
// export function updateStatuBarInfo(tabid, info) {
|
|
||||||
// statusBarTabInfo.update(x => ({
|
|
||||||
// ...x,
|
|
||||||
// [tabid]: info,
|
|
||||||
// }));
|
|
||||||
// }
|
|
||||||
|
|
||||||
export function updateStatuBarInfoItem(tabid, key, item) {
|
|
||||||
statusBarTabInfo.update(tabs => {
|
|
||||||
const items = tabs[tabid] || [];
|
|
||||||
let newItems;
|
|
||||||
if (item == null) {
|
|
||||||
newItems = items.filter(x => x.key != key);
|
|
||||||
} else if (items.find(x => x.key == key)) {
|
|
||||||
newItems = items.map(x => (x.key == key ? { ...item, key } : x));
|
|
||||||
} else {
|
|
||||||
newItems = [...items, { ...item, key }];
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...tabs,
|
|
||||||
[tabid]: newItems,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte';
|
import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte';
|
||||||
@@ -42,6 +12,7 @@
|
|||||||
import { findCommand } from '../commands/runCommand';
|
import { findCommand } from '../commands/runCommand';
|
||||||
import { useConnectionColor } from '../utility/useConnectionColor';
|
import { useConnectionColor } from '../utility/useConnectionColor';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
import { statusBarTabInfo } from '../utility/statusBarStore';
|
||||||
|
|
||||||
$: databaseName = $currentDatabase && $currentDatabase.name;
|
$: databaseName = $currentDatabase && $currentDatabase.name;
|
||||||
$: connection = $currentDatabase && $currentDatabase.connection;
|
$: connection = $currentDatabase && $currentDatabase.connection;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { getContext, onDestroy, onMount } from 'svelte';
|
import { getContext, onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
import uuidv1 from 'uuid/v1';
|
import uuidv1 from 'uuid/v1';
|
||||||
import { updateStatuBarInfoItem } from './StatusBar.svelte';
|
import { updateStatuBarInfoItem } from '../utility/statusBarStore';
|
||||||
|
|
||||||
export let text;
|
export let text;
|
||||||
export let clickable = false;
|
export let clickable = false;
|
||||||
|
|||||||
@@ -5177,6 +5177,11 @@ interpret@1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
||||||
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
|
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
|
||||||
|
|
||||||
|
interval-operations@^1.0.7:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/interval-operations/-/interval-operations-1.0.7.tgz#c935dfed6bb040064d488f542b9a2c0004d7a333"
|
||||||
|
integrity sha512-VBxXaK+DxTt7Hwr8Rhg03bgdyv5OZ0OfH3hotg9fT2jqufF3VOtvkX33n3t6jNjS8tx1jTKBt7fs++SqNd1OQg==
|
||||||
|
|
||||||
invariant@^2.2.4:
|
invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
|
|||||||
Reference in New Issue
Block a user