@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.
Files changed (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /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>