@takodotid/circuit 0.43.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!-- Every device of the project, with where it is, what it runs on, and what validate finds on it. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import type { StudioData } from "../../../types";
|
|
5
|
+
import DataTable, { type Row } from "./DataTable.vue";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ data: StudioData }>();
|
|
8
|
+
|
|
9
|
+
const rows = computed<Row[]>(() =>
|
|
10
|
+
props.data.devices.map((device) => {
|
|
11
|
+
const findings = props.data.findings.filter((finding) => finding.device === device.name);
|
|
12
|
+
const errors = findings.filter((finding) => finding.level === "error").length;
|
|
13
|
+
return {
|
|
14
|
+
cells: [
|
|
15
|
+
{ device: device.name },
|
|
16
|
+
device.site,
|
|
17
|
+
device.platform,
|
|
18
|
+
device.model,
|
|
19
|
+
device.connection.host ?? "console only",
|
|
20
|
+
errors || "",
|
|
21
|
+
findings.length - errors || "",
|
|
22
|
+
props.data.files[device.name],
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<h2>Devices</h2>
|
|
31
|
+
<DataTable :head="['Device', 'Site', 'Platform', 'Model', 'Reached at', 'Errors', 'Warnings', 'File']" :rows="rows" csv="devices" />
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!-- A device's front panel: one square per port of its model, by speed, filled by how the port is used. A square scrolls to the port's row. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import type { Device, Port } from "../../../../model";
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{ device: Device; ports: string[] }>();
|
|
7
|
+
|
|
8
|
+
type Use = "shut" | "lag" | "access" | "trunk" | "routed" | "plain";
|
|
9
|
+
|
|
10
|
+
const USES: Record<Use, string> = {
|
|
11
|
+
shut: "shut down",
|
|
12
|
+
lag: "in a LAG",
|
|
13
|
+
access: "access",
|
|
14
|
+
trunk: "trunk",
|
|
15
|
+
routed: "routed",
|
|
16
|
+
plain: "up, nothing on it",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const useOf = (port: Port | undefined): Use => {
|
|
20
|
+
if (!port) return "shut";
|
|
21
|
+
if (port.in_lag) return "lag";
|
|
22
|
+
if (port.access_vlan) return "access";
|
|
23
|
+
if (port.trunk_vlans) return "trunk";
|
|
24
|
+
if (port.addresses) return "routed";
|
|
25
|
+
return "plain";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const bySpeed = computed(() => {
|
|
29
|
+
const groups = new Map<string, { name: string; use: Use; linked: boolean; label: string }[]>();
|
|
30
|
+
|
|
31
|
+
for (const name of props.ports) {
|
|
32
|
+
const port = props.device.interfaces?.ethernet?.[name];
|
|
33
|
+
const use = useOf(port);
|
|
34
|
+
const far = port?.link;
|
|
35
|
+
const label = [name, USES[use], port?.description, far ? `to ${far.device} ${far.port}` : ""].filter(Boolean).join(", ");
|
|
36
|
+
const speed = name.split("-")[0] ?? name;
|
|
37
|
+
groups.set(speed, [...(groups.get(speed) ?? []), { name, use, linked: Boolean(far), label }]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [...groups];
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function show(name: string): void {
|
|
44
|
+
const row = document.getElementById(`port-${name}`);
|
|
45
|
+
row?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
46
|
+
row?.classList.add("flash");
|
|
47
|
+
setTimeout(() => row?.classList.remove("flash"), 1600);
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div v-if="ports.length">
|
|
53
|
+
<div class="faceplate" role="group" :aria-label="`${device.name}'s ports`">
|
|
54
|
+
<div v-for="[speed, squares] in bySpeed" :key="speed" class="speed">
|
|
55
|
+
<span class="speed-name">{{ speed }}</span>
|
|
56
|
+
<div class="ports">
|
|
57
|
+
<button
|
|
58
|
+
v-for="square in squares"
|
|
59
|
+
:key="square.name"
|
|
60
|
+
:class="['port', square.use, { linked: square.linked }]"
|
|
61
|
+
:title="square.label"
|
|
62
|
+
:aria-label="square.label"
|
|
63
|
+
@click="show(square.name)"
|
|
64
|
+
>
|
|
65
|
+
{{ square.name.split("-")[1] ?? square.name }}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<ul class="faceplate-legend">
|
|
71
|
+
<li v-for="(text, use) in USES" :key="use"><span :class="['port', use]"></span>{{ text }}</li>
|
|
72
|
+
<li><span class="port plain linked"></span>linked to another device</li>
|
|
73
|
+
</ul>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
<!-- The network drawn, one layer at a time: cables, VLANs, address segments, BGP, OSPF and tunnels. Drawn with Cytoscape; a node or line picked shows what it is in the panel. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import cytoscape, { type Core, type ElementDefinition } from "cytoscape";
|
|
4
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
5
|
+
import type { GraphEdge, GraphNode, Layer } from "../../../../core/graph";
|
|
6
|
+
import type { StudioData } from "../../../types";
|
|
7
|
+
import { Export } from "../lib/csv";
|
|
8
|
+
import { go, store } from "../lib/store";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ data: StudioData }>();
|
|
11
|
+
|
|
12
|
+
const LAYERS: { id: Layer; label: string; legend: string }[] = [
|
|
13
|
+
{ id: "l1", label: "Cables", legend: "Each cable a port's link names. A LAG's cables are one line." },
|
|
14
|
+
{ id: "l2", label: "VLANs", legend: "The VLANs each cable carries, from its port or LAG." },
|
|
15
|
+
{
|
|
16
|
+
id: "l3",
|
|
17
|
+
label: "Addresses",
|
|
18
|
+
legend: "Prefixes two devices hold addresses in. Three or more share a segment, drawn as its own box.",
|
|
19
|
+
},
|
|
20
|
+
{ id: "bgp", label: "BGP", legend: "Sessions between our devices, matched by address, and to each AS outside." },
|
|
21
|
+
{ id: "ospf", label: "OSPF", legend: "Interfaces in one area and one prefix, neither passive." },
|
|
22
|
+
{ id: "tunnels", label: "Tunnels", legend: "GRE and VXLAN tunnels matched by their endpoints, and WireGuard peers." },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const layer = computed<Layer>(() => (LAYERS.some((item) => item.id === store.route.parts[0]) ? (store.route.parts[0] as Layer) : "l1"));
|
|
26
|
+
const counts = computed(() =>
|
|
27
|
+
Object.fromEntries(LAYERS.map((item) => [item.id, props.data.graph.edges.filter((edge) => edge.layer === item.id).length]))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const container = ref<HTMLElement>();
|
|
31
|
+
const selected = ref<{ node?: GraphNode; edge?: GraphEdge }>({});
|
|
32
|
+
let graph: Core | undefined;
|
|
33
|
+
|
|
34
|
+
/** Where each node was on each layer, kept while the page is open, so a node moved stays moved. */
|
|
35
|
+
const placed = new Map<string, Record<string, { x: number; y: number }>>();
|
|
36
|
+
|
|
37
|
+
const css = (name: string) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
38
|
+
|
|
39
|
+
function elements(): ElementDefinition[] {
|
|
40
|
+
const edges = props.data.graph.edges.filter((edge) => edge.layer === layer.value);
|
|
41
|
+
const ends = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
|
|
42
|
+
const levelOf = (node: GraphNode) => {
|
|
43
|
+
const findings = props.data.findings.filter((finding) => `device:${finding.device}` === node.id);
|
|
44
|
+
return findings.some((finding) => finding.level === "error") ? "error" : findings.length ? "warning" : "";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
...props.data.graph.nodes
|
|
49
|
+
.filter((node) => node.kind === "device" || ends.has(node.id))
|
|
50
|
+
.map((node) => ({
|
|
51
|
+
group: "nodes" as const,
|
|
52
|
+
// A box as wide as its name.
|
|
53
|
+
data: { id: node.id, label: node.label, kind: node.kind, level: levelOf(node), width: node.label.length * 8.5 + 28 },
|
|
54
|
+
})),
|
|
55
|
+
...edges.map((edge, index) => ({
|
|
56
|
+
group: "edges" as const,
|
|
57
|
+
data: {
|
|
58
|
+
id: `edge-${index}`,
|
|
59
|
+
source: edge.from,
|
|
60
|
+
target: edge.to,
|
|
61
|
+
label: edge.label,
|
|
62
|
+
oneSided: edge.oneSided ? "yes" : "no",
|
|
63
|
+
index,
|
|
64
|
+
},
|
|
65
|
+
})),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function draw(): void {
|
|
70
|
+
if (!graph) return;
|
|
71
|
+
const positions = placed.get(layer.value);
|
|
72
|
+
|
|
73
|
+
graph.elements().remove();
|
|
74
|
+
graph.add(elements());
|
|
75
|
+
selected.value = {};
|
|
76
|
+
|
|
77
|
+
if (positions && graph.nodes().every((node) => positions[node.id()] !== undefined)) {
|
|
78
|
+
graph.nodes().forEach((node) => {
|
|
79
|
+
node.position(positions[node.id()]!);
|
|
80
|
+
});
|
|
81
|
+
fit();
|
|
82
|
+
} else {
|
|
83
|
+
layOut();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Fit the drawing to the window, but never larger than its own size: a network of two devices stays readable. */
|
|
88
|
+
function fit(): void {
|
|
89
|
+
if (!graph) return;
|
|
90
|
+
graph.fit(undefined, 60);
|
|
91
|
+
if (graph.zoom() > 1.2) {
|
|
92
|
+
graph.zoom(1.2);
|
|
93
|
+
graph.center();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function layOut(): void {
|
|
98
|
+
// From a circle rather than random places, so the same network is drawn the same way each time.
|
|
99
|
+
graph?.layout({ name: "circle", animate: false, fit: false }).run();
|
|
100
|
+
// Boxes are wide, so they push hard and keep a gap, or they land on each other and on the lines' labels.
|
|
101
|
+
graph
|
|
102
|
+
?.layout({
|
|
103
|
+
name: "cose",
|
|
104
|
+
animate: false,
|
|
105
|
+
fit: false,
|
|
106
|
+
randomize: false,
|
|
107
|
+
nodeRepulsion: () => 160000,
|
|
108
|
+
nodeOverlap: 40,
|
|
109
|
+
nodeDimensionsIncludeLabels: true,
|
|
110
|
+
idealEdgeLength: () => 55,
|
|
111
|
+
edgeElasticity: () => 200,
|
|
112
|
+
gravity: 2.5,
|
|
113
|
+
numIter: 3000,
|
|
114
|
+
componentSpacing: 140,
|
|
115
|
+
})
|
|
116
|
+
.run();
|
|
117
|
+
fit();
|
|
118
|
+
remember();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function remember(): void {
|
|
122
|
+
if (!graph) return;
|
|
123
|
+
placed.set(layer.value, Object.fromEntries(graph.nodes().map((node) => [node.id(), { ...node.position() }])));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function saveImage(): void {
|
|
127
|
+
if (!graph) return;
|
|
128
|
+
const png = graph.png({ output: "blob", full: true, scale: 2, bg: css("--bg") }) as unknown as Blob;
|
|
129
|
+
Export.save(`${props.data.project}-${layer.value}.png`, png);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onMounted(() => {
|
|
133
|
+
graph = cytoscape({
|
|
134
|
+
container: container.value,
|
|
135
|
+
minZoom: 0.2,
|
|
136
|
+
maxZoom: 3,
|
|
137
|
+
style: [
|
|
138
|
+
{
|
|
139
|
+
selector: "node",
|
|
140
|
+
style: {
|
|
141
|
+
shape: "round-rectangle",
|
|
142
|
+
width: "data(width)",
|
|
143
|
+
height: 34,
|
|
144
|
+
"background-color": css("--surface"),
|
|
145
|
+
"border-width": 2,
|
|
146
|
+
"border-color": css("--text"),
|
|
147
|
+
label: "data(label)",
|
|
148
|
+
color: css("--text"),
|
|
149
|
+
"font-family": css("--font"),
|
|
150
|
+
"font-size": 13,
|
|
151
|
+
"font-weight": 700,
|
|
152
|
+
"text-valign": "center",
|
|
153
|
+
"text-halign": "center",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
selector: 'node[kind = "segment"]',
|
|
158
|
+
style: { "background-color": css("--yellow"), "border-color": css("--navy"), color: "#11141b" },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
selector: 'node[kind = "outside"]',
|
|
162
|
+
style: { "border-style": "dashed", "border-color": css("--edge"), color: css("--muted") },
|
|
163
|
+
},
|
|
164
|
+
{ selector: 'node[level = "warning"]', style: { "border-color": css("--warning"), "border-width": 3 } },
|
|
165
|
+
{ selector: 'node[level = "error"]', style: { "border-color": css("--error"), "border-width": 3 } },
|
|
166
|
+
{
|
|
167
|
+
selector: "edge",
|
|
168
|
+
style: {
|
|
169
|
+
width: 2,
|
|
170
|
+
"line-color": css("--edge"),
|
|
171
|
+
"curve-style": "bezier",
|
|
172
|
+
label: "data(label)",
|
|
173
|
+
"font-size": 11,
|
|
174
|
+
color: css("--muted"),
|
|
175
|
+
"text-background-color": css("--bg"),
|
|
176
|
+
"text-background-opacity": 1,
|
|
177
|
+
"text-background-padding": "2px",
|
|
178
|
+
"font-family": css("--font"),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{ selector: 'edge[oneSided = "yes"]', style: { "line-style": "dashed", "line-color": css("--warning") } },
|
|
182
|
+
{
|
|
183
|
+
selector: ":selected",
|
|
184
|
+
style: { "border-color": css("--cobalt"), "line-color": css("--cobalt"), "border-width": 4, width: 4 },
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
graph.on("tap", "node", (event) => {
|
|
190
|
+
selected.value = { node: props.data.graph.nodes.find((node) => node.id === event.target.id()) };
|
|
191
|
+
});
|
|
192
|
+
graph.on("tap", "edge", (event) => {
|
|
193
|
+
const index = event.target.data("index") as number;
|
|
194
|
+
selected.value = { edge: props.data.graph.edges.filter((edge) => edge.layer === layer.value)[index] };
|
|
195
|
+
});
|
|
196
|
+
graph.on("tap", (event) => {
|
|
197
|
+
if (event.target === graph) selected.value = {};
|
|
198
|
+
});
|
|
199
|
+
graph.on("dbltap", 'node[kind = "device"]', (event) => go(`devices/${event.target.data("label")}`));
|
|
200
|
+
graph.on("dragfree", "node", remember);
|
|
201
|
+
|
|
202
|
+
draw();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
onBeforeUnmount(() => graph?.destroy());
|
|
206
|
+
watch([layer, () => props.data], draw);
|
|
207
|
+
|
|
208
|
+
const nodeFindings = computed(() =>
|
|
209
|
+
props.data.findings.filter((finding) => selected.value.node && `device:${finding.device}` === selected.value.node.id)
|
|
210
|
+
);
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<template>
|
|
214
|
+
<section class="view">
|
|
215
|
+
<div class="toolbar">
|
|
216
|
+
<nav aria-label="Layers">
|
|
217
|
+
<button v-for="item in LAYERS" :key="item.id" :aria-pressed="layer === item.id" @click="go(`graph/${item.id}`)">
|
|
218
|
+
{{ item.label }} <small>{{ counts[item.id] }}</small>
|
|
219
|
+
</button>
|
|
220
|
+
</nav>
|
|
221
|
+
<div class="actions">
|
|
222
|
+
<button title="Fit the drawing to the window" @click="fit">Fit</button>
|
|
223
|
+
<button title="Forget where nodes were moved on this layer, and lay it out again" @click="layOut">Lay out again</button>
|
|
224
|
+
<button title="Save this layer as a PNG image" @click="saveImage">Save image</button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="stage">
|
|
228
|
+
<div ref="container" class="canvas" role="img" aria-label="The network"></div>
|
|
229
|
+
<aside>
|
|
230
|
+
<template v-if="selected.node">
|
|
231
|
+
<h2>{{ selected.node.label }}</h2>
|
|
232
|
+
<p class="kind muted">
|
|
233
|
+
{{
|
|
234
|
+
selected.node.kind === "device"
|
|
235
|
+
? "a device"
|
|
236
|
+
: selected.node.kind === "segment"
|
|
237
|
+
? "a shared segment"
|
|
238
|
+
: "outside the network"
|
|
239
|
+
}}
|
|
240
|
+
</p>
|
|
241
|
+
<ul>
|
|
242
|
+
<li v-for="line in selected.node.detail" :key="line">{{ line }}</li>
|
|
243
|
+
</ul>
|
|
244
|
+
<ul v-if="nodeFindings.length" class="findings">
|
|
245
|
+
<li v-for="(finding, index) in nodeFindings" :key="index">
|
|
246
|
+
<span :class="['badge', finding.level]">{{ finding.level }}</span
|
|
247
|
+
>{{ finding.message }}
|
|
248
|
+
</li>
|
|
249
|
+
</ul>
|
|
250
|
+
<p v-if="selected.node.kind === 'device'">
|
|
251
|
+
<a @click="go(`devices/${selected.node.label}`)">Open {{ selected.node.label }}</a>
|
|
252
|
+
</p>
|
|
253
|
+
</template>
|
|
254
|
+
<template v-else-if="selected.edge">
|
|
255
|
+
<h2>{{ selected.edge.label }}</h2>
|
|
256
|
+
<p v-if="selected.edge.oneSided" class="badge warning">declared at one end only</p>
|
|
257
|
+
<ul>
|
|
258
|
+
<li v-for="line in selected.edge.detail" :key="line">{{ line }}</li>
|
|
259
|
+
</ul>
|
|
260
|
+
</template>
|
|
261
|
+
<template v-else>
|
|
262
|
+
<h2>{{ LAYERS.find((item) => item.id === layer)?.label }}</h2>
|
|
263
|
+
<p class="muted">{{ LAYERS.find((item) => item.id === layer)?.legend }}</p>
|
|
264
|
+
<ul class="legend">
|
|
265
|
+
<li>A dashed line is declared at one end only.</li>
|
|
266
|
+
<li>A dashed box is outside the network.</li>
|
|
267
|
+
<li>A box framed in orange or red has warnings or errors.</li>
|
|
268
|
+
<li>Pick a box or a line to see what it is. Double-click a device to open it.</li>
|
|
269
|
+
</ul>
|
|
270
|
+
</template>
|
|
271
|
+
</aside>
|
|
272
|
+
</div>
|
|
273
|
+
</section>
|
|
274
|
+
</template>
|
|
275
|
+
|
|
276
|
+
<style scoped>
|
|
277
|
+
.canvas {
|
|
278
|
+
flex: 1;
|
|
279
|
+
min-width: 0;
|
|
280
|
+
height: 100%;
|
|
281
|
+
}
|
|
282
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!-- Two texts compared line by line: what is the same is folded away, except a few lines around each change. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import { Diff } from "../lib/diff";
|
|
5
|
+
import { Format } from "../lib/format";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ before: string; after: string }>();
|
|
8
|
+
|
|
9
|
+
const lines = computed(() => Diff.lines(props.before.trimEnd().split("\n"), props.after.trimEnd().split("\n")));
|
|
10
|
+
const changed = computed(() => lines.value?.filter((line) => line.kind !== "same").length ?? 0);
|
|
11
|
+
const shown = computed(() => (lines.value ? Diff.shown(lines.value) : []));
|
|
12
|
+
const mark = (kind: string) => (kind === "added" ? "+ " : kind === "removed" ? "- " : " ");
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<p v-if="!lines" class="muted">They differ in too many lines to compare one by one.</p>
|
|
17
|
+
<p v-else-if="!changed" class="muted">They are the same, line for line.</p>
|
|
18
|
+
<div v-else>
|
|
19
|
+
<p class="muted">{{ Format.count(changed, "line differs", "lines differ") }}.</p>
|
|
20
|
+
<pre
|
|
21
|
+
class="code diff"
|
|
22
|
+
><template v-for="(line, index) in shown" :key="index"><span v-if="line.kind === 'fold'" class="fold">{{ Format.count(line.count, "line") }} the same{{ "\n" }}</span><span v-else :class="line.kind">{{ mark(line.kind) }}{{ line.text }}{{ "\n" }}</span></template></pre>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!-- What `circuit validate` finds, errors first, and what only one end declares: cables and BGP sessions with nothing back. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import type { StudioData } from "../../../types";
|
|
5
|
+
import { go } from "../lib/store";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ data: StudioData }>();
|
|
8
|
+
|
|
9
|
+
const findings = computed(() => [...props.data.findings].sort((a, b) => (a.level === b.level ? 0 : a.level === "error" ? -1 : 1)));
|
|
10
|
+
const oneSidedCables = computed(() => props.data.cables.filter((cable) => cable.oneSided));
|
|
11
|
+
const oneSidedSessions = computed(() => props.data.sessions.filter((session) => session.oneSided));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<h2>Problems</h2>
|
|
16
|
+
<h3>Found by <code>circuit validate</code></h3>
|
|
17
|
+
<p v-if="!findings.length" class="muted">Nothing: every device validates.</p>
|
|
18
|
+
<ul v-else class="findings">
|
|
19
|
+
<li v-for="(finding, index) in findings" :key="index">
|
|
20
|
+
<span :class="['badge', finding.level]">{{ finding.level }}</span>
|
|
21
|
+
<a v-if="finding.device" @click="go(`devices/${finding.device}`)">{{ finding.device }}</a
|
|
22
|
+
><template v-if="finding.device">: </template>{{ finding.message }}
|
|
23
|
+
</li>
|
|
24
|
+
</ul>
|
|
25
|
+
|
|
26
|
+
<h3>Declared at one end only</h3>
|
|
27
|
+
<p v-if="!oneSidedCables.length && !oneSidedSessions.length" class="muted">
|
|
28
|
+
Every cable and BGP session between devices is declared at both ends.
|
|
29
|
+
</p>
|
|
30
|
+
<ul v-else class="findings">
|
|
31
|
+
<li v-for="cable in oneSidedCables" :key="`${cable.device} ${cable.port}`">
|
|
32
|
+
<span class="badge warning">cable</span>
|
|
33
|
+
<a @click="go(`devices/${cable.device}`)">{{ cable.device }}</a> {{ cable.port }} links to {{ cable.farDevice }}
|
|
34
|
+
{{ cable.farPort }}, which does not link back
|
|
35
|
+
</li>
|
|
36
|
+
<li v-for="session in oneSidedSessions" :key="`${session.device} ${session.name}`">
|
|
37
|
+
<span class="badge warning">BGP</span>
|
|
38
|
+
<a @click="go(`devices/${session.device}`)">{{ session.device }}</a> {{ session.name }} reaches {{ session.far }}, which has no
|
|
39
|
+
session back
|
|
40
|
+
</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!-- What the search finds: devices, ports and interfaces by name or description, addresses, VLANs, BGP neighbors, and lines of each device's config. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import type { StudioData } from "../../../types";
|
|
5
|
+
import { Format } from "../lib/format";
|
|
6
|
+
import DataTable, { type Row } from "./DataTable.vue";
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ data: StudioData; query: string }>();
|
|
9
|
+
|
|
10
|
+
const found = computed<Row[]>(() => {
|
|
11
|
+
const query = props.query.toLowerCase();
|
|
12
|
+
if (!query) return [];
|
|
13
|
+
const has = (text: unknown) => typeof text === "string" && text.toLowerCase().includes(query);
|
|
14
|
+
const rows: Row[] = [];
|
|
15
|
+
|
|
16
|
+
for (const device of props.data.devices) {
|
|
17
|
+
if (has(device.name) || has(device.site) || has(device.model)) {
|
|
18
|
+
rows.push({ cells: [{ device: device.name }, "device", `${device.platform} ${device.model} at ${device.site}`] });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const [kind, group] of Object.entries(device.interfaces ?? {})) {
|
|
22
|
+
for (const [name, settings] of Object.entries((group ?? {}) as Record<string, { description?: string } | undefined>)) {
|
|
23
|
+
if (has(name) || has(settings?.description)) {
|
|
24
|
+
rows.push({
|
|
25
|
+
cells: [
|
|
26
|
+
{ device: device.name },
|
|
27
|
+
kind === "ethernet" ? "port" : kind.replace("_", " "),
|
|
28
|
+
`${name}${settings?.description ? `: ${settings.description}` : ""}`,
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const [name, vlan] of Object.entries(device.switching?.vlans ?? {})) {
|
|
36
|
+
if (has(name) || has(vlan?.description) || has(String(vlan?.id)))
|
|
37
|
+
rows.push({ cells: [{ device: device.name }, "VLAN", `${name}, ID ${vlan?.id}`] });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [name, neighbor] of Object.entries(device.routing?.bgp?.neighbors ?? {})) {
|
|
41
|
+
if (has(name) || has(neighbor?.description) || has(neighbor?.address)) {
|
|
42
|
+
rows.push({ cells: [{ device: device.name }, "BGP neighbor", `${name}, ${neighbor?.address}`] });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = (props.data.rendered[device.name] ?? "").split("\n").filter(has);
|
|
47
|
+
if (lines.length) {
|
|
48
|
+
rows.push({
|
|
49
|
+
cells: [
|
|
50
|
+
{ link: `devices/${device.name}/config`, label: device.name },
|
|
51
|
+
"config",
|
|
52
|
+
`${Format.count(lines.length, "line")}: ${lines
|
|
53
|
+
.slice(0, 3)
|
|
54
|
+
.map((line) => line.trim())
|
|
55
|
+
.join(" · ")}${lines.length > 3 ? " …" : ""}`,
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const row of props.data.addresses) {
|
|
62
|
+
if (has(row.address)) rows.push({ cells: [{ device: row.device }, "address", `${row.address} on ${row.holder}`] });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return rows;
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<h2>Search: {{ query }}</h2>
|
|
71
|
+
<DataTable :head="['Device', 'What', 'Found']" :rows="found" />
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<!-- The secrets, file by file: each name and how it is kept. To see the values, a person types the file's password; they are decrypted here, in the browser, and the password is never sent or kept. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { onMounted, reactive, ref } from "vue";
|
|
4
|
+
import type { SecretsFileListing, SecretsListing } from "../../../types";
|
|
5
|
+
import { SecretsCrypto } from "../lib/secrets-crypto";
|
|
6
|
+
import { api } from "../lib/store";
|
|
7
|
+
|
|
8
|
+
const listing = ref<SecretsListing>();
|
|
9
|
+
const loadError = ref("");
|
|
10
|
+
/** By file: the password being typed, why it failed, whether it is being tried, and the values it opened. */
|
|
11
|
+
const typed = reactive<Record<string, string>>({});
|
|
12
|
+
const failed = reactive<Record<string, string>>({});
|
|
13
|
+
const busy = reactive<Record<string, boolean>>({});
|
|
14
|
+
const shown = reactive<Record<string, Record<string, string>>>({});
|
|
15
|
+
|
|
16
|
+
onMounted(async () => {
|
|
17
|
+
try {
|
|
18
|
+
listing.value = await api<SecretsListing>("/api/secrets");
|
|
19
|
+
} catch (error) {
|
|
20
|
+
loadError.value = (error as Error).message;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const encryptedOf = (file: SecretsFileListing) => file.entries.filter((entry) => entry.kind === "encrypted");
|
|
25
|
+
|
|
26
|
+
/** Decrypt every encrypted value of a file with the typed password, then forget the password. */
|
|
27
|
+
async function reveal(file: SecretsFileListing): Promise<void> {
|
|
28
|
+
failed[file.file] = "";
|
|
29
|
+
const crypto = new SecretsCrypto(typed[file.file] ?? "");
|
|
30
|
+
typed[file.file] = "";
|
|
31
|
+
busy[file.file] = true;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const values: Record<string, string> = {};
|
|
35
|
+
for (const entry of encryptedOf(file)) values[entry.name] = await crypto.decrypt(entry.name, entry.ciphertext!);
|
|
36
|
+
shown[file.file] = values;
|
|
37
|
+
} catch {
|
|
38
|
+
failed[file.file] = `That password does not open ${file.file}.`;
|
|
39
|
+
} finally {
|
|
40
|
+
busy[file.file] = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const KIND: Record<string, string> = {
|
|
45
|
+
encrypted: "encrypted",
|
|
46
|
+
"1password": "kept in 1Password",
|
|
47
|
+
empty: "empty",
|
|
48
|
+
"not encrypted": "NOT ENCRYPTED: run circuit secrets encrypt",
|
|
49
|
+
missing: "missing: the project needs it",
|
|
50
|
+
};
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<h2>Secrets</h2>
|
|
55
|
+
<p class="muted">
|
|
56
|
+
Each secret, and how it is kept. Add or change one with <code>circuit secrets</code>. A value is never sent to this page in plain
|
|
57
|
+
text.
|
|
58
|
+
</p>
|
|
59
|
+
<p v-if="loadError" class="verdict reject">{{ loadError }}</p>
|
|
60
|
+
|
|
61
|
+
<section v-for="file in listing?.files ?? []" :key="file.file">
|
|
62
|
+
<h3>
|
|
63
|
+
<code>{{ file.file }}</code>
|
|
64
|
+
</h3>
|
|
65
|
+
<p class="muted">
|
|
66
|
+
{{ file.networks.length > 1 ? "Networks" : "Network" }} {{ file.networks.join(", ") }}. Its password:
|
|
67
|
+
{{ file.passwordLocation ?? "typed when Circuit asks" }}.
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
<form v-if="encryptedOf(file).length" class="tool-form" autocomplete="off" @submit.prevent="reveal(file)">
|
|
71
|
+
<label class="field"
|
|
72
|
+
>The password of {{ file.file }}
|
|
73
|
+
<input v-model="typed[file.file]" type="password" autocomplete="off" spellcheck="false" />
|
|
74
|
+
<small>Used here in the browser to decrypt, then forgotten. It is never sent anywhere or kept.</small>
|
|
75
|
+
</label>
|
|
76
|
+
<button class="primary" type="submit" :disabled="busy[file.file]">
|
|
77
|
+
{{ busy[file.file] ? "Opening…" : "Show the values" }}
|
|
78
|
+
</button>
|
|
79
|
+
<button v-if="shown[file.file]" type="button" @click="delete shown[file.file]">Hide them</button>
|
|
80
|
+
</form>
|
|
81
|
+
<p v-if="failed[file.file]" class="verdict reject">{{ failed[file.file] }}</p>
|
|
82
|
+
|
|
83
|
+
<p v-if="!file.entries.length" class="muted">No secrets yet.</p>
|
|
84
|
+
<table v-else class="data">
|
|
85
|
+
<thead>
|
|
86
|
+
<tr>
|
|
87
|
+
<th>Name</th>
|
|
88
|
+
<th>Kept</th>
|
|
89
|
+
<th>Value</th>
|
|
90
|
+
</tr>
|
|
91
|
+
</thead>
|
|
92
|
+
<tbody>
|
|
93
|
+
<tr v-for="entry in file.entries" :key="entry.name">
|
|
94
|
+
<td>
|
|
95
|
+
<code>{{ entry.name }}</code>
|
|
96
|
+
</td>
|
|
97
|
+
<td>
|
|
98
|
+
<span v-if="entry.kind === 'not encrypted' || entry.kind === 'missing'" class="badge error">{{
|
|
99
|
+
KIND[entry.kind]
|
|
100
|
+
}}</span>
|
|
101
|
+
<template v-else>{{ KIND[entry.kind] }}</template>
|
|
102
|
+
</td>
|
|
103
|
+
<td>
|
|
104
|
+
<template v-if="entry.onePassword">vault {{ entry.onePassword.vault }}, item {{ entry.onePassword.item }}</template>
|
|
105
|
+
<code v-else-if="shown[file.file]?.[entry.name] !== undefined">{{ shown[file.file]![entry.name] }}</code>
|
|
106
|
+
<span v-else class="muted">{{ entry.kind === "encrypted" ? "hidden" : "" }}</span>
|
|
107
|
+
</td>
|
|
108
|
+
</tr>
|
|
109
|
+
</tbody>
|
|
110
|
+
</table>
|
|
111
|
+
</section>
|
|
112
|
+
</template>
|