@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,9 @@
|
|
|
1
|
+
// A `.vue` file, to TypeScript: a component. Vite compiles it.
|
|
2
|
+
declare module "*.vue" {
|
|
3
|
+
import type { DefineComponent } from "vue";
|
|
4
|
+
const component: DefineComponent<object, object, unknown>;
|
|
5
|
+
export default component;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// A stylesheet imported for its effect: Vite puts it in the page.
|
|
9
|
+
declare module "*.css";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- Circuit Studio. The app fills the page from /api/data, and draws again when the server says the project changed. -->
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Circuit Studio</title>
|
|
8
|
+
<!-- Studio shows one team's network, so search engines are asked to leave it out. -->
|
|
9
|
+
<meta name="robots" content="noindex, nofollow, noarchive" />
|
|
10
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
11
|
+
<!-- Special Gothic is shared with the documentation site, and served from beside this page so Studio works offline. -->
|
|
12
|
+
<link rel="preload" href="/special-gothic-latin.woff2" as="font" type="font/woff2" crossorigin />
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="app"></div>
|
|
16
|
+
<script type="module" src="/src/main.ts"></script>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!-- Studio's frame: the rail with the views and the search, the network's health, and the view the address names. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed, ref, watch } from "vue";
|
|
4
|
+
import { Format } from "./lib/format";
|
|
5
|
+
import { go, store } from "./lib/store";
|
|
6
|
+
import AddressesView from "./components/AddressesView.vue";
|
|
7
|
+
import CablesView from "./components/CablesView.vue";
|
|
8
|
+
import CompareView from "./components/CompareView.vue";
|
|
9
|
+
import DeviceView from "./components/DeviceView.vue";
|
|
10
|
+
import DevicesView from "./components/DevicesView.vue";
|
|
11
|
+
import GraphView from "./components/GraphView.vue";
|
|
12
|
+
import ProblemsView from "./components/ProblemsView.vue";
|
|
13
|
+
import SearchView from "./components/SearchView.vue";
|
|
14
|
+
import SecretsView from "./components/SecretsView.vue";
|
|
15
|
+
import SessionsView from "./components/SessionsView.vue";
|
|
16
|
+
import ToolsView from "./components/ToolsView.vue";
|
|
17
|
+
import VlansView from "./components/VlansView.vue";
|
|
18
|
+
|
|
19
|
+
const VIEWS = [
|
|
20
|
+
{ id: "graph", label: "Graph" },
|
|
21
|
+
{ id: "devices", label: "Devices" },
|
|
22
|
+
{ id: "vlans", label: "VLANs" },
|
|
23
|
+
{ id: "addresses", label: "Addresses" },
|
|
24
|
+
{ id: "sessions", label: "BGP" },
|
|
25
|
+
{ id: "cables", label: "Cables" },
|
|
26
|
+
{ id: "tools", label: "Tools" },
|
|
27
|
+
{ id: "compare", label: "Compare" },
|
|
28
|
+
{ id: "secrets", label: "Secrets" },
|
|
29
|
+
{ id: "problems", label: "Problems" },
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
// `#device/<name>` opens a device too, as `#devices/<name>` does.
|
|
33
|
+
const view = computed(() => (store.route.view === "device" ? "devices" : store.route.view));
|
|
34
|
+
const device = computed(() => (view.value === "devices" ? store.route.parts[0] : undefined));
|
|
35
|
+
|
|
36
|
+
const errors = computed(() => store.data?.findings.filter((finding) => finding.level === "error").length ?? 0);
|
|
37
|
+
const warnings = computed(() => (store.data?.findings.length ?? 0) - errors.value);
|
|
38
|
+
|
|
39
|
+
const summary = computed(() => [
|
|
40
|
+
{ text: Format.count(store.data?.devices.length ?? 0, "device"), state: "neutral" },
|
|
41
|
+
{ text: Format.count(errors.value, "error"), state: errors.value ? "error" : "quiet" },
|
|
42
|
+
{ text: Format.count(warnings.value, "warning"), state: warnings.value ? "warning" : "quiet" },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// The search waits for typing to stop, then shows what it finds; an emptied search goes back to the graph.
|
|
46
|
+
const query = ref(view.value === "search" ? (store.route.parts[0] ?? "") : "");
|
|
47
|
+
let searching: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
watch(query, (text) => {
|
|
49
|
+
clearTimeout(searching);
|
|
50
|
+
searching = setTimeout(() => go(text.trim() ? `search/${text.trim()}` : "graph"), 250);
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<header>
|
|
56
|
+
<a class="brand" href="#graph" aria-label="Circuit Studio, back to the graph"
|
|
57
|
+
><span class="wordmark">Circuit</span><span class="product">Studio</span></a
|
|
58
|
+
>
|
|
59
|
+
<nav aria-label="Views">
|
|
60
|
+
<button v-for="item in VIEWS" :key="item.id" :aria-pressed="view === item.id" @click="go(item.id)">
|
|
61
|
+
{{ item.label }}<span v-if="item.id === 'problems'" class="count">{{ store.data?.findings.length || "" }}</span>
|
|
62
|
+
</button>
|
|
63
|
+
</nav>
|
|
64
|
+
<input v-model="query" type="search" placeholder="Search the project" aria-label="Search the project" />
|
|
65
|
+
<div class="status-rail" aria-label="Network status">
|
|
66
|
+
<span class="summary">
|
|
67
|
+
<span v-for="item in summary" :key="item.text" :class="['summary-item', item.state]">{{ item.text }}</span>
|
|
68
|
+
</span>
|
|
69
|
+
<span :class="['live', { on: store.live === 'live', beat: store.beat }]">{{
|
|
70
|
+
store.live === "live" ? `${store.data?.project ?? ""} · live` : store.live
|
|
71
|
+
}}</span>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
|
|
75
|
+
<pre v-if="store.error" class="banner">{{ store.error }}</pre>
|
|
76
|
+
|
|
77
|
+
<main v-if="store.data">
|
|
78
|
+
<GraphView v-if="view === 'graph'" :data="store.data" />
|
|
79
|
+
<DeviceView v-else-if="device" :key="device" :data="store.data" :name="device" />
|
|
80
|
+
<div v-else class="tables">
|
|
81
|
+
<DevicesView v-if="view === 'devices'" :data="store.data" />
|
|
82
|
+
<VlansView v-else-if="view === 'vlans'" :data="store.data" />
|
|
83
|
+
<AddressesView v-else-if="view === 'addresses'" :data="store.data" />
|
|
84
|
+
<SessionsView v-else-if="view === 'sessions'" :data="store.data" />
|
|
85
|
+
<CablesView v-else-if="view === 'cables'" :data="store.data" />
|
|
86
|
+
<ToolsView v-else-if="view === 'tools'" :data="store.data" />
|
|
87
|
+
<CompareView v-else-if="view === 'compare'" :data="store.data" />
|
|
88
|
+
<SecretsView v-else-if="view === 'secrets'" />
|
|
89
|
+
<ProblemsView v-else-if="view === 'problems'" :data="store.data" />
|
|
90
|
+
<SearchView v-else-if="view === 'search'" :data="store.data" :query="store.route.parts[0] ?? ''" />
|
|
91
|
+
<p v-else class="muted">Nothing here. <a @click="go('graph')">Back to the graph</a>.</p>
|
|
92
|
+
</div>
|
|
93
|
+
</main>
|
|
94
|
+
<main v-else-if="!store.error" class="tables"><p class="muted">Reading the project…</p></main>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!-- Every address a device holds, by the prefix it is in, with an address held twice marked. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed, ref } from "vue";
|
|
4
|
+
import type { StudioData } from "../../../types";
|
|
5
|
+
import { Net } from "../lib/net";
|
|
6
|
+
import DataTable, { type Row } from "./DataTable.vue";
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ data: StudioData }>();
|
|
9
|
+
const family = ref<"all" | "ipv4" | "ipv6">("all");
|
|
10
|
+
|
|
11
|
+
const rows = computed<Row[]>(() => {
|
|
12
|
+
const shown = props.data.addresses.filter((row) => family.value === "all" || row.family === family.value);
|
|
13
|
+
// The same address in the same table on two holders.
|
|
14
|
+
const twice = (row: (typeof shown)[number]) =>
|
|
15
|
+
props.data.addresses.filter((other) => other.vrf === row.vrf && Net.sameAddress(other.address, row.address)).length > 1;
|
|
16
|
+
|
|
17
|
+
const sorted = [...shown].sort((a, b) => {
|
|
18
|
+
const x = Net.parseRange(a.address);
|
|
19
|
+
const y = Net.parseRange(b.address);
|
|
20
|
+
if (!x || !y || x.family !== y.family) return a.family.localeCompare(b.family);
|
|
21
|
+
return x.first < y.first ? -1 : x.first > y.first ? 1 : 0;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return sorted.map((row) => ({
|
|
25
|
+
cells: [row.prefix, row.address, { device: row.device }, row.holder, row.vrf ?? "", row.peer ?? "", twice(row) ? "held twice" : ""],
|
|
26
|
+
}));
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<h2>Addresses</h2>
|
|
32
|
+
<div class="tabs">
|
|
33
|
+
<button v-for="choice in ['all', 'ipv4', 'ipv6'] as const" :key="choice" :aria-pressed="family === choice" @click="family = choice">
|
|
34
|
+
{{ choice === "all" ? "Both" : choice === "ipv4" ? "IPv4" : "IPv6" }}
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
<DataTable :head="['Prefix', 'Address', 'Device', 'On', 'VRF', 'Far end', 'Note']" :rows="rows" csv="addresses" />
|
|
38
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!-- Every cable the project names with `link`, from the end that names it first. -->
|
|
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
|
+
const devices = computed(() => new Set(props.data.devices.map((device) => device.name)));
|
|
9
|
+
|
|
10
|
+
const rows = computed<Row[]>(() =>
|
|
11
|
+
props.data.cables.map((cable) => ({
|
|
12
|
+
cells: [
|
|
13
|
+
{ device: cable.device },
|
|
14
|
+
cable.port + (cable.lag ? ` (${cable.lag})` : ""),
|
|
15
|
+
devices.value.has(cable.farDevice) ? { device: cable.farDevice } : cable.farDevice,
|
|
16
|
+
cable.farPort + (cable.farLag ? ` (${cable.farLag})` : ""),
|
|
17
|
+
cable.through,
|
|
18
|
+
cable.vlans.join(", "),
|
|
19
|
+
cable.description,
|
|
20
|
+
cable.oneSided ? "the far end does not link back" : "",
|
|
21
|
+
],
|
|
22
|
+
}))
|
|
23
|
+
);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<h2>Cables</h2>
|
|
28
|
+
<DataTable :head="['Device', 'Port', 'Far device', 'Far port', 'Through', 'VLANs', 'Description', 'Note']" :rows="rows" csv="cables" />
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<!-- The project now against another branch, tag or commit: devices added, removed and changed with their config line by line, the graph's lines added and gone, and what validate finds newly or no longer. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed, onMounted, ref, watch } from "vue";
|
|
4
|
+
import type { GraphEdge } from "../../../../core/graph";
|
|
5
|
+
import type { Commit, StudioData } from "../../../types";
|
|
6
|
+
import { Format } from "../lib/format";
|
|
7
|
+
import { api, go, store } from "../lib/store";
|
|
8
|
+
import LineDiff from "./LineDiff.vue";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ data: StudioData }>();
|
|
11
|
+
|
|
12
|
+
const refs = ref<{ names: string[]; commits: Commit[] }>();
|
|
13
|
+
const refsError = ref("");
|
|
14
|
+
const typed = ref(store.route.parts[0] ?? "");
|
|
15
|
+
const comparison = ref<{ commit: Commit; network: StudioData }>();
|
|
16
|
+
const compareError = ref("");
|
|
17
|
+
const reading = ref(false);
|
|
18
|
+
|
|
19
|
+
onMounted(async () => {
|
|
20
|
+
try {
|
|
21
|
+
refs.value = await api("/api/refs");
|
|
22
|
+
} catch (error) {
|
|
23
|
+
refsError.value = (error as Error).message;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
watch(
|
|
28
|
+
() => store.route.parts[0],
|
|
29
|
+
async (asked) => {
|
|
30
|
+
comparison.value = undefined;
|
|
31
|
+
compareError.value = "";
|
|
32
|
+
if (!asked) return;
|
|
33
|
+
|
|
34
|
+
reading.value = true;
|
|
35
|
+
try {
|
|
36
|
+
const answer = await api<{ commit: Commit; network: StudioData | { error: string } }>(
|
|
37
|
+
`/api/compare?ref=${encodeURIComponent(asked)}`
|
|
38
|
+
);
|
|
39
|
+
if ("error" in answer.network) throw new Error(`The project at ${asked} cannot be read:\n${answer.network.error}`);
|
|
40
|
+
comparison.value = { commit: answer.commit, network: answer.network };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
compareError.value = (error as Error).message;
|
|
43
|
+
} finally {
|
|
44
|
+
reading.value = false;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{ immediate: true }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const devices = computed(() => {
|
|
51
|
+
const then = comparison.value?.network;
|
|
52
|
+
if (!then) return undefined;
|
|
53
|
+
|
|
54
|
+
const before = new Map(then.devices.map((device) => [device.name, then.rendered[device.name] ?? ""]));
|
|
55
|
+
const after = new Map(props.data.devices.map((device) => [device.name, props.data.rendered[device.name] ?? ""]));
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
added: [...after.keys()].filter((name) => !before.has(name)),
|
|
59
|
+
removed: [...before.keys()].filter((name) => !after.has(name)),
|
|
60
|
+
changed: [...after.keys()]
|
|
61
|
+
.filter((name) => before.has(name) && before.get(name) !== after.get(name))
|
|
62
|
+
.map((name) => ({ name, before: before.get(name)!, after: after.get(name)! })),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const edgeKey = (edge: GraphEdge) => `${edge.layer}|${[edge.from, edge.to].sort().join("|")}|${edge.label}`;
|
|
67
|
+
const edges = computed(() => {
|
|
68
|
+
const then = comparison.value?.network;
|
|
69
|
+
if (!then) return undefined;
|
|
70
|
+
|
|
71
|
+
const before = new Set(then.graph.edges.map(edgeKey));
|
|
72
|
+
const after = new Set(props.data.graph.edges.map(edgeKey));
|
|
73
|
+
const describe = (edge: GraphEdge) =>
|
|
74
|
+
`${edge.layer}: ${edge.from.replace(/^\w+:/, "")} to ${edge.to.replace(/^\w+:/, "")}, ${edge.label}`;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
added: props.data.graph.edges.filter((edge) => !before.has(edgeKey(edge))).map(describe),
|
|
78
|
+
gone: then.graph.edges.filter((edge) => !after.has(edgeKey(edge))).map(describe),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const findingKey = (finding: { level: string; device?: string; message: string }) =>
|
|
83
|
+
`${finding.level}|${finding.device ?? ""}|${finding.message}`;
|
|
84
|
+
const findings = computed(() => {
|
|
85
|
+
const then = comparison.value?.network;
|
|
86
|
+
if (!then) return undefined;
|
|
87
|
+
|
|
88
|
+
const before = new Set(then.findings.map(findingKey));
|
|
89
|
+
const after = new Set(props.data.findings.map(findingKey));
|
|
90
|
+
return {
|
|
91
|
+
added: props.data.findings.filter((finding) => !before.has(findingKey(finding))),
|
|
92
|
+
gone: then.findings.filter((finding) => !after.has(findingKey(finding))),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<template>
|
|
98
|
+
<h2>Compare</h2>
|
|
99
|
+
<p class="muted">
|
|
100
|
+
The project now against a branch, tag or commit, read by the Circuit running now: so it shows changes to your files, not to Circuit.
|
|
101
|
+
Before you merge a pull request, compare its branch with <code>main</code>.
|
|
102
|
+
</p>
|
|
103
|
+
<p v-if="refsError" class="verdict reject">{{ refsError }}</p>
|
|
104
|
+
<form class="tool-form" @submit.prevent="typed.trim() && go(`compare/${typed.trim()}`)">
|
|
105
|
+
<label class="field"
|
|
106
|
+
>Branch, tag or commit
|
|
107
|
+
<input v-model="typed" list="refs" placeholder="main, HEAD~3" />
|
|
108
|
+
<datalist id="refs">
|
|
109
|
+
<option v-for="name in refs?.names ?? []" :key="name" :value="name" />
|
|
110
|
+
<option v-for="commit in refs?.commits ?? []" :key="commit.commit" :value="commit.short">{{ commit.subject }}</option>
|
|
111
|
+
</datalist>
|
|
112
|
+
</label>
|
|
113
|
+
<button class="primary" type="submit">Compare</button>
|
|
114
|
+
</form>
|
|
115
|
+
|
|
116
|
+
<p v-if="reading" class="muted">Reading the project at {{ store.route.parts[0] }}…</p>
|
|
117
|
+
<pre v-if="compareError" class="banner">{{ compareError }}</pre>
|
|
118
|
+
|
|
119
|
+
<template v-if="comparison && devices && edges && findings">
|
|
120
|
+
<p>
|
|
121
|
+
Against <strong>{{ comparison.commit.short }}</strong
|
|
122
|
+
>, {{ comparison.commit.subject }}, by {{ comparison.commit.author }}, {{ new Date(comparison.commit.date).toLocaleString() }}.
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
<h3>Devices</h3>
|
|
126
|
+
<p v-if="!devices.added.length && !devices.removed.length && !devices.changed.length" class="muted">No device's config changes.</p>
|
|
127
|
+
<ul class="findings">
|
|
128
|
+
<li v-for="name in devices.added" :key="`added ${name}`">
|
|
129
|
+
<span class="badge">added</span><a @click="go(`devices/${name}`)">{{ name }}</a>
|
|
130
|
+
</li>
|
|
131
|
+
<li v-for="name in devices.removed" :key="`removed ${name}`"><span class="badge error">removed</span>{{ name }}</li>
|
|
132
|
+
</ul>
|
|
133
|
+
<details v-for="change in devices.changed" :key="change.name" class="change">
|
|
134
|
+
<summary>{{ change.name }}: its config changes</summary>
|
|
135
|
+
<LineDiff :before="change.before" :after="change.after" />
|
|
136
|
+
</details>
|
|
137
|
+
|
|
138
|
+
<h3>The graph</h3>
|
|
139
|
+
<p v-if="!edges.added.length && !edges.gone.length" class="muted">No line of the graph changes.</p>
|
|
140
|
+
<ul class="findings">
|
|
141
|
+
<li v-for="line in edges.added" :key="`+${line}`"><span class="added">+</span> {{ line }}</li>
|
|
142
|
+
<li v-for="line in edges.gone" :key="`-${line}`"><span class="removed">−</span> {{ line }}</li>
|
|
143
|
+
</ul>
|
|
144
|
+
|
|
145
|
+
<h3>Found by <code>circuit validate</code></h3>
|
|
146
|
+
<p v-if="!findings.added.length && !findings.gone.length" class="muted">Nothing new, and nothing gone.</p>
|
|
147
|
+
<p v-else class="muted">
|
|
148
|
+
{{ Format.count(findings.added.length, "new finding") }}, {{ Format.count(findings.gone.length, "finding") }} no longer.
|
|
149
|
+
</p>
|
|
150
|
+
<ul class="findings">
|
|
151
|
+
<li v-for="(finding, index) in findings.added" :key="`new ${index}`">
|
|
152
|
+
<span :class="['badge', finding.level]">new {{ finding.level }}</span
|
|
153
|
+
>{{ finding.device ? `${finding.device}: ` : "" }}{{ finding.message }}
|
|
154
|
+
</li>
|
|
155
|
+
<li v-for="(finding, index) in findings.gone" :key="`gone ${index}`">
|
|
156
|
+
<span class="badge">no longer</span>{{ finding.device ? `${finding.device}: ` : "" }}{{ finding.message }}
|
|
157
|
+
</li>
|
|
158
|
+
</ul>
|
|
159
|
+
</template>
|
|
160
|
+
</template>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!-- A table of rows, each cell text or a link, that can be saved as CSV. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { Export } from "../lib/csv";
|
|
4
|
+
import { go } from "../lib/store";
|
|
5
|
+
|
|
6
|
+
export type Cell = string | number | undefined | { device: string; label?: string } | { link: string; label: string };
|
|
7
|
+
export type Row = { cells: Cell[]; muted?: boolean; id?: string };
|
|
8
|
+
export type Column = string | { label: string; field?: boolean; title?: string };
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ head: Column[]; rows: Row[]; csv?: string }>();
|
|
11
|
+
|
|
12
|
+
const labelOf = (column: Column) => (typeof column === "string" ? column : column.label);
|
|
13
|
+
const textOf = (cell: Cell) =>
|
|
14
|
+
cell === undefined ? "" : typeof cell === "object" ? ("device" in cell ? (cell.label ?? cell.device) : cell.label) : String(cell);
|
|
15
|
+
const linkOf = (cell: Cell) => (typeof cell === "object" && cell ? ("device" in cell ? `devices/${cell.device}` : cell.link) : undefined);
|
|
16
|
+
|
|
17
|
+
function save(): void {
|
|
18
|
+
const csv = Export.csv(
|
|
19
|
+
props.head.map(labelOf),
|
|
20
|
+
props.rows.map((row) => row.cells.map(textOf))
|
|
21
|
+
);
|
|
22
|
+
Export.save(`${props.csv}.csv`, new Blob([csv], { type: "text/csv" }));
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<p v-if="!rows.length" class="muted">None.</p>
|
|
28
|
+
<div v-else class="table-block">
|
|
29
|
+
<p v-if="csv" class="table-actions"><a @click="save">Save as CSV</a></p>
|
|
30
|
+
<table class="data">
|
|
31
|
+
<thead>
|
|
32
|
+
<tr>
|
|
33
|
+
<th v-for="column in head" :key="labelOf(column)" :title="typeof column === 'string' ? undefined : column.title">
|
|
34
|
+
<code v-if="typeof column !== 'string' && column.field">{{ column.label }}</code>
|
|
35
|
+
<template v-else>{{ labelOf(column) }}</template>
|
|
36
|
+
</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
<tr v-for="(row, index) in rows" :id="row.id" :key="index" :class="{ muted: row.muted }">
|
|
41
|
+
<td v-for="(cell, at) in row.cells" :key="at">
|
|
42
|
+
<a v-if="linkOf(cell)" @click="go(linkOf(cell)!)">{{ textOf(cell) }}</a>
|
|
43
|
+
<template v-else>{{ textOf(cell) }}</template>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
</tbody>
|
|
47
|
+
</table>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<!-- One device: what validate finds on it, each group of its settings, what it would run, what it runs now when asked, and the commits that changed its file. -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed, ref, watch } from "vue";
|
|
4
|
+
import type { Device } from "../../../../model";
|
|
5
|
+
import type { Commit, LiveRead, StudioData } from "../../../types";
|
|
6
|
+
import { Format } from "../lib/format";
|
|
7
|
+
import { api, go, store } from "../lib/store";
|
|
8
|
+
import DataTable, { type Row } from "./DataTable.vue";
|
|
9
|
+
import Faceplate from "./Faceplate.vue";
|
|
10
|
+
import LineDiff from "./LineDiff.vue";
|
|
11
|
+
import Settings from "./Settings.vue";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{ data: StudioData; name: string }>();
|
|
14
|
+
|
|
15
|
+
const device = computed<Device | undefined>(() => props.data.devices.find((candidate) => candidate.name === props.name));
|
|
16
|
+
const findings = computed(() => props.data.findings.filter((finding) => finding.device === props.name));
|
|
17
|
+
const errors = computed(() => findings.value.filter((finding) => finding.level === "error").length);
|
|
18
|
+
|
|
19
|
+
/** A section shows when the device has anything in it. */
|
|
20
|
+
const SECTIONS = [
|
|
21
|
+
{ id: "overview", title: "Overview", has: () => true },
|
|
22
|
+
{ id: "ports", title: "Ports", has: () => true },
|
|
23
|
+
{ id: "interfaces", title: "Interfaces", has: (d: Device) => Object.keys(d.interfaces ?? {}).some((kind) => kind !== "ethernet") },
|
|
24
|
+
{ id: "switching", title: "Switching", has: (d: Device) => Boolean(d.switching) },
|
|
25
|
+
{ id: "routing", title: "Routing", has: (d: Device) => Boolean(d.routing) },
|
|
26
|
+
{ id: "firewall", title: "Firewall", has: (d: Device) => Boolean(d.firewall) },
|
|
27
|
+
{ id: "management", title: "Management", has: (d: Device) => Boolean(d.management) },
|
|
28
|
+
{ id: "system", title: "System", has: (d: Device) => Boolean(d.system) },
|
|
29
|
+
{ id: "monitoring", title: "Monitoring", has: (d: Device) => Boolean(d.monitoring) },
|
|
30
|
+
{ id: "services", title: "Services", has: (d: Device) => Boolean(d.services) },
|
|
31
|
+
{ id: "config", title: "Config", has: () => true },
|
|
32
|
+
{ id: "now", title: "On the device now", has: () => true },
|
|
33
|
+
{ id: "history", title: "History", has: () => true },
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
const sections = computed(() => (device.value ? SECTIONS.filter((section) => section.has(device.value!)) : []));
|
|
37
|
+
const section = computed(() => store.route.parts[1] ?? "overview");
|
|
38
|
+
|
|
39
|
+
const ports = computed<Row[]>(() => {
|
|
40
|
+
const own = device.value;
|
|
41
|
+
if (!own) return [];
|
|
42
|
+
|
|
43
|
+
const declared = Object.entries(own.interfaces?.ethernet ?? {});
|
|
44
|
+
const all = props.data.ports[own.name]?.length ? props.data.ports[own.name]! : declared.map(([name]) => name);
|
|
45
|
+
|
|
46
|
+
return all.map((name) => {
|
|
47
|
+
const port = own.interfaces?.ethernet?.[name];
|
|
48
|
+
if (!port) return { id: `port-${name}`, muted: true, cells: [name, "shut down", "", "", "", ""] };
|
|
49
|
+
|
|
50
|
+
const lag = port.in_lag ? own.interfaces?.lag?.[port.in_lag] : undefined;
|
|
51
|
+
const carrier = lag ?? port;
|
|
52
|
+
const switching = carrier.access_vlan
|
|
53
|
+
? `access ${carrier.access_vlan}`
|
|
54
|
+
: carrier.trunk_vlans
|
|
55
|
+
? `trunk ${carrier.trunk_vlans.join(", ")}${carrier.native_vlan ? `, native ${carrier.native_vlan}` : ""}`
|
|
56
|
+
: "";
|
|
57
|
+
const { description, addresses, link, in_lag, access_vlan, trunk_vlans, native_vlan, ...rest } = port;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
id: `port-${name}`,
|
|
61
|
+
cells: [
|
|
62
|
+
name,
|
|
63
|
+
description ?? "",
|
|
64
|
+
[switching, in_lag ? `in LAG ${in_lag}` : ""].filter(Boolean).join(", "),
|
|
65
|
+
Format.compact(addresses),
|
|
66
|
+
link ? { device: link.device, label: `${link.device} ${link.port}${link.through ? `, through ${link.through}` : ""}` } : "",
|
|
67
|
+
Format.compact(rest),
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const interfaces = computed(() => {
|
|
74
|
+
const { ethernet: _ethernet, ...rest } = device.value?.interfaces ?? {};
|
|
75
|
+
return rest;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Reading the device reaches it, so it happens only when asked, and nothing is kept.
|
|
79
|
+
const live = ref<LiveRead>();
|
|
80
|
+
const reading = ref(false);
|
|
81
|
+
const readError = ref("");
|
|
82
|
+
|
|
83
|
+
async function readNow(): Promise<void> {
|
|
84
|
+
reading.value = true;
|
|
85
|
+
readError.value = "";
|
|
86
|
+
try {
|
|
87
|
+
live.value = await api<LiveRead>(`/api/read?device=${encodeURIComponent(props.name)}`, true);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
readError.value = (error as Error).message;
|
|
90
|
+
} finally {
|
|
91
|
+
reading.value = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const commands = computed(() => live.value?.steps.reduce((count, step) => count + step.show.length, 0) ?? 0);
|
|
96
|
+
|
|
97
|
+
const history = ref<Commit[]>();
|
|
98
|
+
const historyError = ref("");
|
|
99
|
+
watch(
|
|
100
|
+
section,
|
|
101
|
+
async (id) => {
|
|
102
|
+
if (id !== "history" || history.value) return;
|
|
103
|
+
try {
|
|
104
|
+
history.value = await api<Commit[]>(`/api/history?device=${encodeURIComponent(props.name)}`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
historyError.value = (error as Error).message;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{ immediate: true }
|
|
110
|
+
);
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<template>
|
|
114
|
+
<div class="tables">
|
|
115
|
+
<p class="crumbs"><a @click="go('devices')">Devices</a> /</p>
|
|
116
|
+
<p v-if="!device" class="muted">No device {{ name }} in the project.</p>
|
|
117
|
+
<template v-else>
|
|
118
|
+
<h2>
|
|
119
|
+
{{ device.name }}
|
|
120
|
+
<span v-if="errors" class="badge error">{{ Format.count(errors, "error") }}</span>
|
|
121
|
+
<span v-if="findings.length - errors" class="badge warning">{{ Format.count(findings.length - errors, "warning") }}</span>
|
|
122
|
+
</h2>
|
|
123
|
+
<p class="muted">
|
|
124
|
+
{{ device.platform }} {{ device.model }} at {{ device.site }} · <code>{{ data.files[device.name] }}</code>
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
<div class="tabs">
|
|
128
|
+
<button
|
|
129
|
+
v-for="item in sections"
|
|
130
|
+
:key="item.id"
|
|
131
|
+
:aria-pressed="section === item.id"
|
|
132
|
+
@click="go(`devices/${device.name}/${item.id}`)"
|
|
133
|
+
>
|
|
134
|
+
{{ item.title }}
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<template v-if="section === 'overview'">
|
|
139
|
+
<template v-if="findings.length">
|
|
140
|
+
<h3>Found by <code>circuit validate</code></h3>
|
|
141
|
+
<ul class="findings">
|
|
142
|
+
<li v-for="(finding, index) in findings" :key="index">
|
|
143
|
+
<span :class="['badge', finding.level]">{{ finding.level }}</span
|
|
144
|
+
>{{ finding.message }}
|
|
145
|
+
</li>
|
|
146
|
+
</ul>
|
|
147
|
+
</template>
|
|
148
|
+
<h3>Device</h3>
|
|
149
|
+
<Settings
|
|
150
|
+
:value="{
|
|
151
|
+
name: device.name,
|
|
152
|
+
site: device.site,
|
|
153
|
+
role: device.role,
|
|
154
|
+
platform: device.platform,
|
|
155
|
+
model: device.model,
|
|
156
|
+
connection: device.connection,
|
|
157
|
+
}"
|
|
158
|
+
/>
|
|
159
|
+
</template>
|
|
160
|
+
|
|
161
|
+
<template v-else-if="section === 'ports'">
|
|
162
|
+
<Faceplate :device="device" :ports="data.ports[device.name] ?? []" />
|
|
163
|
+
<DataTable
|
|
164
|
+
:head="['Port', 'Description', 'Switching', 'Addresses', 'Link', 'Other settings']"
|
|
165
|
+
:rows="ports"
|
|
166
|
+
:csv="`${device.name}-ports`"
|
|
167
|
+
/>
|
|
168
|
+
</template>
|
|
169
|
+
|
|
170
|
+
<Settings v-else-if="section === 'interfaces'" :value="interfaces" />
|
|
171
|
+
<Settings v-else-if="section === 'switching'" :value="device.switching" />
|
|
172
|
+
<Settings v-else-if="section === 'routing'" :value="device.routing" />
|
|
173
|
+
<Settings v-else-if="section === 'firewall'" :value="device.firewall" />
|
|
174
|
+
<Settings v-else-if="section === 'management'" :value="device.management" />
|
|
175
|
+
<Settings v-else-if="section === 'system'" :value="device.system" />
|
|
176
|
+
<Settings v-else-if="section === 'monitoring'" :value="device.monitoring" />
|
|
177
|
+
<Settings v-else-if="section === 'services'" :value="device.services" />
|
|
178
|
+
|
|
179
|
+
<template v-else-if="section === 'config'">
|
|
180
|
+
<p class="muted">
|
|
181
|
+
What the device should run, as Circuit writes it for {{ device.platform }}. A secret shows as its name, never its value.
|
|
182
|
+
</p>
|
|
183
|
+
<pre class="code">{{ data.rendered[device.name] }}</pre>
|
|
184
|
+
</template>
|
|
185
|
+
|
|
186
|
+
<template v-else-if="section === 'now'">
|
|
187
|
+
<p class="muted">
|
|
188
|
+
Circuit reads the device now, the same way <code>circuit diff</code> does, and shows what
|
|
189
|
+
<code>circuit apply</code> would send. Nothing is kept: read it again to see it again.
|
|
190
|
+
</p>
|
|
191
|
+
<p>
|
|
192
|
+
<button class="primary" :disabled="reading" @click="readNow">
|
|
193
|
+
{{ reading ? "Reading the device…" : "Read from the device now" }}
|
|
194
|
+
</button>
|
|
195
|
+
</p>
|
|
196
|
+
<p v-if="readError" class="verdict reject">{{ readError }}</p>
|
|
197
|
+
<template v-if="live">
|
|
198
|
+
<p class="muted">Read at {{ new Date(live.readAt).toLocaleString() }}.</p>
|
|
199
|
+
<h3>What apply would send</h3>
|
|
200
|
+
<p v-if="!commands && !live.problems.length" class="verdict accept">
|
|
201
|
+
The device already runs its config. Nothing to change.
|
|
202
|
+
</p>
|
|
203
|
+
<template v-for="step in live.steps" :key="step.title">
|
|
204
|
+
<h4>{{ step.title }}</h4>
|
|
205
|
+
<pre class="code">{{ step.show.join("\n") }}</pre>
|
|
206
|
+
</template>
|
|
207
|
+
<ul v-if="live.problems.length" class="findings">
|
|
208
|
+
<li v-for="problem in live.problems" :key="problem"><span class="badge error">problem</span>{{ problem }}</li>
|
|
209
|
+
</ul>
|
|
210
|
+
<h3>What it runs, against the project</h3>
|
|
211
|
+
<LineDiff :before="live.running" :after="live.rendered" />
|
|
212
|
+
</template>
|
|
213
|
+
</template>
|
|
214
|
+
|
|
215
|
+
<template v-else-if="section === 'history'">
|
|
216
|
+
<p v-if="historyError" class="muted">{{ historyError }}</p>
|
|
217
|
+
<p v-else-if="!history" class="muted">Reading the history…</p>
|
|
218
|
+
<p v-else-if="!history.length" class="muted">No commit changed {{ data.files[device.name] }} yet.</p>
|
|
219
|
+
<DataTable
|
|
220
|
+
v-else
|
|
221
|
+
:head="['Commit', 'When', 'Who', 'What', '']"
|
|
222
|
+
:rows="
|
|
223
|
+
history.map((commit) => ({
|
|
224
|
+
cells: [
|
|
225
|
+
commit.short,
|
|
226
|
+
new Date(commit.date).toLocaleString(),
|
|
227
|
+
commit.author,
|
|
228
|
+
commit.subject,
|
|
229
|
+
{ link: `compare/${commit.commit}`, label: 'compare with now' },
|
|
230
|
+
],
|
|
231
|
+
}))
|
|
232
|
+
"
|
|
233
|
+
/>
|
|
234
|
+
</template>
|
|
235
|
+
</template>
|
|
236
|
+
</div>
|
|
237
|
+
</template>
|