Add new files and initial code
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
/out/
|
||||
/resources/public/js/compiled/
|
||||
/target/
|
||||
/*-init.clj
|
||||
/*.log
|
||||
|
||||
# Leiningen
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
|
||||
# Node.js dependencies
|
||||
/node_modules/
|
||||
|
||||
# shadow-cljs cache, port files
|
||||
/.shadow-cljs/
|
||||
|
||||
.clj-kondo
|
||||
.lsp
|
196
README.md
Normal file
196
README.md
Normal file
@ -0,0 +1,196 @@
|
||||
# demo
|
||||
|
||||
A [re-frame](https://github.com/day8/re-frame) application designed to ... well, that part is up to
|
||||
you.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Project Overview
|
||||
|
||||
* Architecture:
|
||||
[Single Page Application (SPA)](https://en.wikipedia.org/wiki/Single-page_application)
|
||||
* Languages
|
||||
- Front end is [ClojureScript](https://clojurescript.org/) with ([re-frame](https://github.com/day8/re-frame))
|
||||
* Dependencies
|
||||
- UI framework: [re-frame](https://github.com/day8/re-frame)
|
||||
([docs](https://github.com/day8/re-frame/blob/master/docs/README.md),
|
||||
[FAQs](https://github.com/day8/re-frame/blob/master/docs/FAQs/README.md)) ->
|
||||
[Reagent](https://github.com/reagent-project/reagent) ->
|
||||
[React](https://github.com/facebook/react)
|
||||
- Client-side routing: [bidi](https://github.com/juxt/bidi) and [pushy](https://github.com/clj-commons/pushy)
|
||||
* Build tools
|
||||
- CLJS compilation, dependency management, REPL, & hot reload: [`shadow-cljs`](https://github.com/thheller/shadow-cljs)
|
||||
* Development tools
|
||||
- Debugging: [CLJS DevTools](https://github.com/binaryage/cljs-devtools),
|
||||
[re-frisk](https://github.com/flexsurfer/re-frisk)
|
||||
|
||||
#### Directory structure
|
||||
|
||||
* [`/`](/../../): project config files
|
||||
* [`dev/`](dev/): source files compiled only with the [dev](#running-the-app) profile
|
||||
- [`user.cljs`](dev/cljs/user.cljs): symbols for use during development in the
|
||||
[ClojureScript REPL](#connecting-to-the-browser-repl-from-a-terminal)
|
||||
* [`resources/public/`](resources/public/): SPA root directory;
|
||||
[dev](#running-the-app) / [prod](#production) profile depends on the most recent build
|
||||
- [`index.html`](resources/public/index.html): SPA home page
|
||||
- Dynamic SPA content rendered in the following `div`:
|
||||
```html
|
||||
<div id="app"></div>
|
||||
```
|
||||
- Customizable; add headers, footers, links to other scripts and styles, etc.
|
||||
- Generated directories and files
|
||||
- Created on build with either the [dev](#running-the-app) or [prod](#production) profile
|
||||
- `js/compiled/`: compiled CLJS (`shadow-cljs`)
|
||||
- Not tracked in source control; see [`.gitignore`](.gitignore)
|
||||
* [`src/demo/`](src/demo/): SPA source files (ClojureScript,
|
||||
[re-frame](https://github.com/Day8/re-frame))
|
||||
- [`core.cljs`](src/demo/core.cljs): contains the SPA entry point, `init`
|
||||
* [`.github/workflows/`](.github/workflows/): contains the
|
||||
[github actions](https://github.com/features/actions) pipelines.
|
||||
- [`test.yaml`](.github/workflows/test.yaml): Pipeline for testing.
|
||||
|
||||
|
||||
### Editor/IDE
|
||||
|
||||
Use your preferred editor or IDE that supports Clojure/ClojureScript development. See
|
||||
[Clojure tools](https://clojure.org/community/resources#_clojure_tools) for some popular options.
|
||||
|
||||
### Environment Setup
|
||||
|
||||
1. Install [JDK 8 or later](https://openjdk.java.net/install/) (Java Development Kit)
|
||||
2. Install [Node.js](https://nodejs.org/) (JavaScript runtime environment) which should include
|
||||
[NPM](https://docs.npmjs.com/cli/npm) or if your Node.js installation does not include NPM also install it.
|
||||
5. Clone this repo and open a terminal in the `demo` project root directory
|
||||
|
||||
### Browser Setup
|
||||
|
||||
Browser caching should be disabled when developer tools are open to prevent interference with
|
||||
[`shadow-cljs`](https://github.com/thheller/shadow-cljs) hot reloading.
|
||||
|
||||
Custom formatters must be enabled in the browser before
|
||||
[CLJS DevTools](https://github.com/binaryage/cljs-devtools) can display ClojureScript data in the
|
||||
console in a more readable way.
|
||||
|
||||
#### Chrome/Chromium
|
||||
|
||||
1. Open [DevTools](https://developers.google.com/web/tools/chrome-devtools/) (Linux/Windows: `F12`
|
||||
or `Ctrl-Shift-I`; macOS: `⌘-Option-I`)
|
||||
2. Open DevTools Settings (Linux/Windows: `?` or `F1`; macOS: `?` or `Fn+F1`)
|
||||
3. Select `Preferences` in the navigation menu on the left, if it is not already selected
|
||||
4. Under the `Network` heading, enable the `Disable cache (while DevTools is open)` option
|
||||
5. Under the `Console` heading, enable the `Enable custom formatters` option
|
||||
|
||||
#### Firefox
|
||||
|
||||
1. Open [Developer Tools](https://developer.mozilla.org/en-US/docs/Tools) (Linux/Windows: `F12` or
|
||||
`Ctrl-Shift-I`; macOS: `⌘-Option-I`)
|
||||
2. Open [Developer Tools Settings](https://developer.mozilla.org/en-US/docs/Tools/Settings)
|
||||
(Linux/macOS/Windows: `F1`)
|
||||
3. Under the `Advanced settings` heading, enable the `Disable HTTP Cache (when toolbox is open)`
|
||||
option
|
||||
|
||||
Unfortunately, Firefox does not yet support custom formatters in their devtools. For updates, follow
|
||||
the enhancement request in their bug tracker:
|
||||
[1262914 - Add support for Custom Formatters in devtools](https://bugzilla.mozilla.org/show_bug.cgi?id=1262914).
|
||||
|
||||
## Development
|
||||
|
||||
### Running the App
|
||||
|
||||
Start a temporary local web server, build the app with the `dev` profile, and serve the app,
|
||||
browser test runner and karma test runner with hot reload:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npx shadow-cljs watch app
|
||||
```
|
||||
|
||||
Please be patient; it may take over 20 seconds to see any output, and over 40 seconds to complete.
|
||||
|
||||
When `[:app] Build completed` appears in the output, browse to
|
||||
[http://localhost:8280/](http://localhost:8280/).
|
||||
|
||||
[`shadow-cljs`](https://github.com/thheller/shadow-cljs) will automatically push ClojureScript code
|
||||
changes to your browser on save. To prevent a few common issues, see
|
||||
[Hot Reload in ClojureScript: Things to avoid](https://code.thheller.com/blog/shadow-cljs/2019/08/25/hot-reload-in-clojurescript.html#things-to-avoid).
|
||||
|
||||
Opening the app in your browser starts a
|
||||
[ClojureScript browser REPL](https://clojurescript.org/reference/repl#using-the-browser-as-an-evaluation-environment),
|
||||
to which you may now connect.
|
||||
|
||||
#### Connecting to the browser REPL from your editor
|
||||
|
||||
See
|
||||
[Shadow CLJS User's Guide: Editor Integration](https://shadow-cljs.github.io/docs/UsersGuide.html#_editor_integration).
|
||||
Note that `npm run watch` runs `npx shadow-cljs watch` for you, and that this project's running build ids is
|
||||
`app`, `browser-test`, `karma-test`, or the keywords `:app`, `:browser-test`, `:karma-test` in a Clojure context.
|
||||
|
||||
Alternatively, search the web for info on connecting to a `shadow-cljs` ClojureScript browser REPL
|
||||
from your editor and configuration.
|
||||
|
||||
For example, in Vim / Neovim with `fireplace.vim`
|
||||
1. Open a `.cljs` file in the project to activate `fireplace.vim`
|
||||
2. In normal mode, execute the `Piggieback` command with this project's running build id, `:app`:
|
||||
```vim
|
||||
:Piggieback :app
|
||||
```
|
||||
|
||||
#### Connecting to the browser REPL from a terminal
|
||||
|
||||
1. Connect to the `shadow-cljs` nREPL:
|
||||
```sh
|
||||
lein repl :connect localhost:8777
|
||||
```
|
||||
The REPL prompt, `shadow.user=>`, indicates that is a Clojure REPL, not ClojureScript.
|
||||
|
||||
2. In the REPL, switch the session to this project's running build id, `:app`:
|
||||
```clj
|
||||
(shadow.cljs.devtools.api/nrepl-select :app)
|
||||
```
|
||||
The REPL prompt changes to `cljs.user=>`, indicating that this is now a ClojureScript REPL.
|
||||
3. See [`user.cljs`](dev/cljs/user.cljs) for symbols that are immediately accessible in the REPL
|
||||
without needing to `require`.
|
||||
|
||||
### Running `shadow-cljs` Actions
|
||||
|
||||
See a list of [`shadow-cljs CLI`](https://shadow-cljs.github.io/docs/UsersGuide.html#_command_line)
|
||||
actions:
|
||||
```sh
|
||||
npx shadow-cljs --help
|
||||
```
|
||||
|
||||
Please be patient; it may take over 10 seconds to see any output. Also note that some actions shown
|
||||
may not actually be supported, outputting "Unknown action." when run.
|
||||
|
||||
Run a shadow-cljs action on this project's build id (without the colon, just `app`):
|
||||
```sh
|
||||
npx shadow-cljs <action> app
|
||||
```
|
||||
### Debug Logging
|
||||
|
||||
The `debug?` variable in [`config.cljs`](src/cljs/demo/config.cljs) defaults to `true` in
|
||||
[`dev`](#running-the-app) builds, and `false` in [`prod`](#production) builds.
|
||||
|
||||
Use `debug?` for logging or other tasks that should run only on `dev` builds:
|
||||
|
||||
```clj
|
||||
(ns demo.example
|
||||
(:require [demo.config :as config])
|
||||
|
||||
(when config/debug?
|
||||
(println "This message will appear in the browser console only on dev builds."))
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the app with the `prod` profile:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run release
|
||||
```
|
||||
|
||||
Please be patient; it may take over 15 seconds to see any output, and over 30 seconds to complete.
|
||||
|
||||
The `resources/public/js/compiled` directory is created, containing the compiled `app.js` and
|
||||
`manifest.edn` files.
|
8
dev/user.cljs
Normal file
8
dev/user.cljs
Normal file
@ -0,0 +1,8 @@
|
||||
(ns cljs.user
|
||||
"Commonly used symbols for easy access in the ClojureScript REPL during
|
||||
development."
|
||||
(:require
|
||||
[cljs.repl :refer (Error->map apropos dir doc error->str ex-str ex-triage
|
||||
find-doc print-doc pst source)]
|
||||
[clojure.pprint :refer (pprint)]
|
||||
[clojure.string :as str]))
|
27
karma.conf.js
Normal file
27
karma.conf.js
Normal file
@ -0,0 +1,27 @@
|
||||
module.exports = function (config) {
|
||||
var junitOutputDir = process.env.CIRCLE_TEST_REPORTS || "target/junit"
|
||||
|
||||
config.set({
|
||||
browsers: ['ChromeHeadless'],
|
||||
basePath: 'target',
|
||||
files: ['karma-test.js'],
|
||||
frameworks: ['cljs-test'],
|
||||
plugins: [
|
||||
'karma-cljs-test',
|
||||
'karma-chrome-launcher',
|
||||
'karma-junit-reporter'
|
||||
],
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
client: {
|
||||
args: ['shadow.test.karma.init']
|
||||
},
|
||||
|
||||
// the default configuration
|
||||
junitReporter: {
|
||||
outputDir: junitOutputDir + '/karma', // results will be saved as outputDir/browserName.xml
|
||||
outputFile: undefined, // if included, results will be saved as outputDir/browserName/outputFile
|
||||
suite: '' // suite will become the package name attribute in xml testsuite element
|
||||
}
|
||||
})
|
||||
}
|
1197
package-lock.json
generated
Normal file
1197
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
Normal file
16
package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "demo",
|
||||
"scripts": {
|
||||
"ancient": "clojure -Sdeps '{:deps {com.github.liquidz/antq {:mvn/version \"RELEASE\"}}}' -m antq.core",
|
||||
"watch": "npx shadow-cljs watch app browser-test karma-test",
|
||||
"release": "npx shadow-cljs release app",
|
||||
"build-report": "npx shadow-cljs run shadow.cljs.build-report app target/build-report.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "2.26.2"
|
||||
}
|
||||
}
|
18
resources/public/index.html
Normal file
18
resources/public/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Swagger Merger and PDF Generator</title>
|
||||
<link href="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/bulma/0.9.3/css/bulma.min.css" type="text/css" rel="stylesheet" />
|
||||
<script src="/rapipdf.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
demo is a JavaScript app. Please enable JavaScript to continue.
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<rapi-pdf id="gen-pdf" style="display: none;"></rapi-pdf>
|
||||
<script src="/js/compiled/app.js"></script>
|
||||
</body>
|
||||
</html>
|
95
resources/public/rapipdf.js
Normal file
95
resources/public/rapipdf.js
Normal file
File diff suppressed because one or more lines are too long
33
shadow-cljs.edn
Normal file
33
shadow-cljs.edn
Normal file
@ -0,0 +1,33 @@
|
||||
{:nrepl {:port 8777}
|
||||
|
||||
|
||||
|
||||
:source-paths ["src" "test"]
|
||||
|
||||
:dependencies
|
||||
[[reagent "1.1.1"]
|
||||
[re-frame "1.4.2"]
|
||||
[bidi "2.1.6"]
|
||||
[clj-commons/pushy "0.3.10"]
|
||||
|
||||
[binaryage/devtools "1.0.6"]
|
||||
[re-frisk "1.6.0"]]
|
||||
|
||||
:dev-http
|
||||
{8280 "resources/public"
|
||||
8290 "target/browser-test"}
|
||||
|
||||
:builds
|
||||
{:app
|
||||
{:target :browser
|
||||
:output-dir "resources/public/js/compiled"
|
||||
:asset-path "/js/compiled"
|
||||
:modules
|
||||
{:app {:init-fn demo.core/init}}
|
||||
:devtools
|
||||
{:preloads [
|
||||
re-frisk.preload]}
|
||||
:dev
|
||||
{:compiler-options
|
||||
{:closure-defines
|
||||
{ }}}}}}
|
4
src/demo/config.cljs
Normal file
4
src/demo/config.cljs
Normal file
@ -0,0 +1,4 @@
|
||||
(ns demo.config)
|
||||
|
||||
(def debug?
|
||||
^boolean goog.DEBUG)
|
26
src/demo/core.cljs
Normal file
26
src/demo/core.cljs
Normal file
@ -0,0 +1,26 @@
|
||||
(ns demo.core
|
||||
(:require
|
||||
[reagent.dom :as rdom]
|
||||
[re-frame.core :as re-frame]
|
||||
[demo.events :as events]
|
||||
[demo.routes :as routes]
|
||||
[demo.views :as views]
|
||||
[demo.config :as config]
|
||||
))
|
||||
|
||||
|
||||
(defn dev-setup []
|
||||
(when config/debug?
|
||||
(println "dev mode")))
|
||||
|
||||
(defn ^:dev/after-load mount-root []
|
||||
(re-frame/clear-subscription-cache!)
|
||||
(let [root-el (.getElementById js/document "app")]
|
||||
(rdom/unmount-component-at-node root-el)
|
||||
(rdom/render [views/main-panel] root-el)))
|
||||
|
||||
(defn init []
|
||||
(routes/start!)
|
||||
(re-frame/dispatch-sync [::events/initialize-db])
|
||||
(dev-setup)
|
||||
(mount-root))
|
4
src/demo/db.cljs
Normal file
4
src/demo/db.cljs
Normal file
@ -0,0 +1,4 @@
|
||||
(ns demo.db)
|
||||
|
||||
(def default-db
|
||||
{:name "re-frame"})
|
1049
src/demo/demo.cljs
Normal file
1049
src/demo/demo.cljs
Normal file
File diff suppressed because it is too large
Load Diff
20
src/demo/events.cljs
Normal file
20
src/demo/events.cljs
Normal file
@ -0,0 +1,20 @@
|
||||
(ns demo.events
|
||||
(:require
|
||||
[re-frame.core :as re-frame]
|
||||
[demo.db :as db]
|
||||
))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::initialize-db
|
||||
(fn [_ _]
|
||||
db/default-db))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::navigate
|
||||
(fn [_ [_ handler]]
|
||||
{:navigate handler}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::set-active-panel
|
||||
(fn [{:keys [db]} [_ active-panel]]
|
||||
{:db (assoc db :active-panel active-panel)}))
|
278
src/demo/merge.cljs
Normal file
278
src/demo/merge.cljs
Normal file
@ -0,0 +1,278 @@
|
||||
(ns demo.merge
|
||||
(:require [clojure.string :as str]))
|
||||
|
||||
#_(def ice-map
|
||||
{"borderRouteDetection.json" {:desc "运维监控 / 运维工具 / 网络诊断"
|
||||
:order 26}
|
||||
"deviceinspection.json" {:desc "运维监控 / 运维工具 / 设备巡检"
|
||||
:order 25}
|
||||
"directconnect.json" {:desc "虚拟网络 / 云专线"
|
||||
:order 11}
|
||||
"elastic.json" {:desc "系统概况 / 概况"
|
||||
:order 1}
|
||||
"elk.json" {:desc "运维监控 / ELK监控"
|
||||
:order 16}
|
||||
"emergency.json" {:desc "物理网络 / 交换机 / 特殊操作"
|
||||
:order 4}
|
||||
"epg.json" {:desc "虚拟网络 / EPG终端组"
|
||||
:order 10}
|
||||
"inspection.json" {:desc "运维监控 / 运维工具 / 健康巡检"
|
||||
:order 23}
|
||||
"ipsla.json" {:desc "运维监控 / 运维工具 / IPSLA"
|
||||
:order 20}
|
||||
"l47-nocloud-rest.json" {:desc "网络服务 / L47非云业务"
|
||||
:order 13}
|
||||
"l47service.json" {:desc "网络服务 / L47设备和云业务"
|
||||
:order 14}
|
||||
"mtu.json" {:desc "运维监控 / 运维工具 / MTU检测"
|
||||
:order 21}
|
||||
"networks-map-view-rest.json" {:desc "系统概况 / 三网互视"
|
||||
:order 2}
|
||||
"ngoam.json" {:desc "运维监控 / 运维工具 / 路径检测"
|
||||
:order 24}
|
||||
"northSouthTraffic.json" {:desc "运维监控 / 流量统计 / 南北向流量"
|
||||
:order 19}
|
||||
"overlaymapper.json" {:desc "虚拟网络"
|
||||
:order 8}
|
||||
"physicalresource.json" {:desc "物理网络 / 配置详情 / 交换机和服务器"
|
||||
:order 5}
|
||||
"portmirror.json" {:desc "运维监控 / 运维工具 / SPAN探测"
|
||||
:order 22}
|
||||
"resourcepool.json" {:desc "物理网络 / 资源池和资源管理"
|
||||
:order 6}
|
||||
"servermanage.json" {:desc "物理网络 / 服务器管理"
|
||||
:order 7}
|
||||
"sfc.json" {:desc "网路服务 / 服务链"
|
||||
:order 15}
|
||||
"sys.json" {:desc "系统工具、日志和权限"
|
||||
:order 28}
|
||||
"telemetry.json" {:desc "运维监控 / TELEMETRY监控"
|
||||
:order 17}
|
||||
"traffic-view.json" {:desc "运维监控 / 流量统计 / VXLAN流量"
|
||||
:order 18}
|
||||
"troubleshooting.json" {:desc "运维监控 / 运维工具 / 智能巡检"
|
||||
:order 27}
|
||||
"underlay.json" {:desc "物理网络"
|
||||
:order 3}
|
||||
"vmware.json" {:desc "虚拟网络 / Vmware"
|
||||
:order 12}
|
||||
"vnnb.json" {:desc "虚拟网络 / 非云服务"
|
||||
:order 9}})
|
||||
|
||||
(defn make-unique! [from-data prefix file-pfx file-order]
|
||||
(let [{:keys [swagger info basePath tags schemes paths securityDefinitions definitions]} from-data
|
||||
concat-path (fn [kw]
|
||||
(let [k (name kw)
|
||||
a (str/ends-with? basePath "/")
|
||||
b (str/starts-with? k "/")]
|
||||
(cond (and a b)
|
||||
(keyword (str basePath (subs k 1)))
|
||||
(and (not a) (not b))
|
||||
(keyword (str basePath "/" k))
|
||||
:else
|
||||
(keyword (str basePath k)))))
|
||||
trans-name (fn [old-name] (str/upper-case (str file-pfx old-name)))
|
||||
tags-map (reduce (fn [agg {:keys [name]}] (assoc agg name (trans-name name))) {} tags)
|
||||
tags (mapv
|
||||
(fn [{:keys [name]}]
|
||||
{:name (trans-name name)
|
||||
:order file-order})
|
||||
tags)
|
||||
paths (reduce-kv (fn [m k v] (assoc m (concat-path k) v)) {} paths)
|
||||
paths (reduce-kv
|
||||
(fn [m k v] ;path methods
|
||||
(assoc
|
||||
m k
|
||||
(reduce-kv
|
||||
(fn [m k v] ;method definition
|
||||
(assoc
|
||||
m k
|
||||
(reduce-kv
|
||||
(fn [m k v] ;parameter schema
|
||||
(cond (and (= :parameters k)
|
||||
(vector? v))
|
||||
(assoc
|
||||
m k
|
||||
(mapv (fn [x] ;one parameter map
|
||||
(reduce-kv
|
||||
(fn [m k v] ;schema ref
|
||||
(if (and (= :schema k)
|
||||
(map? v)
|
||||
(contains? v :$ref))
|
||||
(assoc m k
|
||||
(assoc v
|
||||
:$ref
|
||||
(let [ref (get v :$ref)]
|
||||
(if (and (string? ref)
|
||||
(re-find #"^#/definitions/.*" ref))
|
||||
(str "#/definitions/" prefix (subs ref 14))
|
||||
ref))))
|
||||
(assoc m k v)))
|
||||
{}
|
||||
x))
|
||||
v))
|
||||
(and (= :responses k)
|
||||
(map? v))
|
||||
;遍历所有的 XXX: map 中的 map,并当 key 为 schema 且
|
||||
;additionalProperties 为 map 且包含 $ref 时进行替换
|
||||
(assoc
|
||||
m k
|
||||
(reduce-kv
|
||||
(fn [m k v] ;code response
|
||||
(if (and (map? v)
|
||||
(contains? v :schema)
|
||||
(map? (:schema v))
|
||||
(contains? (:schema v) :additionalProperties)
|
||||
(map? (:additionalProperties (:schema v)))
|
||||
(contains? (:additionalProperties (:schema v)) :$ref))
|
||||
(assoc
|
||||
m k
|
||||
(assoc v
|
||||
:schema
|
||||
(assoc (:schema v)
|
||||
:additionalProperties
|
||||
(assoc (:additionalProperties (:schema v))
|
||||
:$ref
|
||||
(let [ref (get (:additionalProperties (:schema v)) :$ref)]
|
||||
(if (and (string? ref)
|
||||
(re-find #"^#/definitions/.*" ref))
|
||||
(str "#/definitions/" prefix (subs ref 14))
|
||||
ref))))))
|
||||
(assoc m k v)))
|
||||
{}
|
||||
v))
|
||||
(and (= :tags k)
|
||||
(vector? v))
|
||||
(assoc m k
|
||||
(mapv
|
||||
(fn [x] (if (contains? tags-map x) (get tags-map x) x))
|
||||
v))
|
||||
:else
|
||||
(assoc m k v)))
|
||||
{}
|
||||
v)))
|
||||
{}
|
||||
v)))
|
||||
{}
|
||||
paths)
|
||||
definitions (reduce-kv (fn [m k v]
|
||||
(assoc m (keyword (str prefix (name k))) v))
|
||||
{}
|
||||
definitions)
|
||||
;如果 definitions 的 properties 中某个 key 的 value map 中包含 $ref,将其改为 #/definitions/ZZZ-xxx
|
||||
definitions (reduce-kv
|
||||
(fn [m k v] ;className properties
|
||||
(assoc m k
|
||||
(if (map? v)
|
||||
(reduce-kv
|
||||
(fn [m k v] ;type/properties...
|
||||
(if (and (= :properties k)
|
||||
(map? v))
|
||||
(assoc
|
||||
m k
|
||||
(reduce-kv
|
||||
(fn [m k v] ;propertyName property
|
||||
(cond (and (map? v)
|
||||
(contains? v :$ref)) ;如果 property 中包含直接 $ref
|
||||
(assoc
|
||||
m k
|
||||
(assoc v
|
||||
:$ref
|
||||
(let [ref (get v :$ref)]
|
||||
(if (and (string? ref)
|
||||
(re-find #"^#/definitions/.*" ref))
|
||||
(str "#/definitions/" prefix (subs ref 14))
|
||||
ref))))
|
||||
(and (map? v)
|
||||
(contains? v :items)
|
||||
(contains? (:items v) :$ref)) ;如果 property 中 items 中包含 $ref
|
||||
(assoc
|
||||
m k
|
||||
(assoc v
|
||||
:items
|
||||
(assoc (:items v)
|
||||
:$ref
|
||||
(let [ref (get (:items v) :$ref)]
|
||||
(if (and (string? ref)
|
||||
(re-find #"^#/definitions/.*" ref))
|
||||
(str "#/definitions/" prefix (subs ref 14))
|
||||
ref)))))
|
||||
:else
|
||||
(assoc m k v)))
|
||||
{}
|
||||
v))
|
||||
(assoc m k v)))
|
||||
{}
|
||||
v)
|
||||
v)))
|
||||
{}
|
||||
definitions)
|
||||
result {:swagger swagger
|
||||
:info info
|
||||
:basePath basePath
|
||||
:tags tags
|
||||
:schemes schemes
|
||||
:paths paths
|
||||
:securityDefinitions securityDefinitions
|
||||
:definitions definitions}]
|
||||
result))
|
||||
|
||||
(defn random-prefix []
|
||||
(.substring (str (random-uuid)) 0 9))
|
||||
|
||||
(defn merge! [data {:keys [mapping document-title
|
||||
document-desc
|
||||
document-version]}]
|
||||
(let [datas (mapv
|
||||
(fn [[name content]]
|
||||
(let [{:keys [desc order]} (get mapping (keyword name))
|
||||
file-pfx (if desc (str desc " / ") "")
|
||||
file-order (or order 0)]
|
||||
(make-unique! content (random-prefix) file-pfx file-order)))
|
||||
data)
|
||||
merged-data (reduce
|
||||
(fn [agg m1]
|
||||
(let [{swagger-1 :swagger
|
||||
info-1 :info
|
||||
basePath-1 :basePath
|
||||
schemes-1 :schemes
|
||||
securityDefinitions-1 :securityDefinitions
|
||||
tags-1 :tags
|
||||
paths-1 :paths
|
||||
definitions-1 :definitions} agg
|
||||
{basePath-2 :basePath
|
||||
swagger-2 :swagger
|
||||
info-2 :info
|
||||
tags-2 :tags
|
||||
schemes-2 :schemes
|
||||
paths-2 :paths
|
||||
securityDefinitions-2 :securityDefinitions
|
||||
definitions-2 :definitions} m1]
|
||||
{:swagger swagger-2
|
||||
:info info-2
|
||||
:basePath "/"
|
||||
:tags (set (into (or tags-1 []) (or tags-2 [])))
|
||||
:schemes schemes-2
|
||||
:paths (merge paths-1 paths-2)
|
||||
:securityDefinitions securityDefinitions-2
|
||||
:definitions (merge definitions-1 definitions-2)}))
|
||||
{}
|
||||
datas)
|
||||
merged-data (assoc
|
||||
merged-data
|
||||
:tags
|
||||
(->> merged-data
|
||||
:tags
|
||||
(vec)
|
||||
(sort-by :order)
|
||||
(mapv (fn [x] (dissoc x :order))))
|
||||
:info
|
||||
(merge (:info merged-data)
|
||||
(if-not (or (nil? document-version)
|
||||
(str/blank? document-version))
|
||||
{:description (or document-desc "")
|
||||
:title document-title
|
||||
:version document-version}
|
||||
{:description (or document-desc "")
|
||||
:title document-title})))]
|
||||
merged-data))
|
43
src/demo/routes.cljs
Normal file
43
src/demo/routes.cljs
Normal file
@ -0,0 +1,43 @@
|
||||
(ns demo.routes
|
||||
(:require
|
||||
[bidi.bidi :as bidi]
|
||||
[pushy.core :as pushy]
|
||||
[re-frame.core :as re-frame]
|
||||
[demo.events :as events]))
|
||||
|
||||
(defmulti panels identity)
|
||||
(defmethod panels :default [] [:div "No panel found for this route."])
|
||||
|
||||
(def routes
|
||||
(atom
|
||||
["/" {"" :home
|
||||
"about" :about}]))
|
||||
|
||||
(defn parse
|
||||
[url]
|
||||
(bidi/match-route @routes url))
|
||||
|
||||
(defn url-for
|
||||
[& args]
|
||||
(apply bidi/path-for (into [@routes] args)))
|
||||
|
||||
(defn dispatch
|
||||
[route]
|
||||
(let [panel (keyword (str (name (:handler route)) "-panel"))]
|
||||
(re-frame/dispatch [::events/set-active-panel panel])))
|
||||
|
||||
(defonce history
|
||||
(pushy/pushy dispatch parse))
|
||||
|
||||
(defn navigate!
|
||||
[handler]
|
||||
(pushy/set-token! history (url-for handler)))
|
||||
|
||||
(defn start!
|
||||
[]
|
||||
(pushy/start! history))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:navigate
|
||||
(fn [handler]
|
||||
(navigate! handler)))
|
13
src/demo/subs.cljs
Normal file
13
src/demo/subs.cljs
Normal file
@ -0,0 +1,13 @@
|
||||
(ns demo.subs
|
||||
(:require
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::name
|
||||
(fn [db]
|
||||
(:name db)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::active-panel
|
||||
(fn [db _]
|
||||
(:active-panel db)))
|
261
src/demo/views.cljs
Normal file
261
src/demo/views.cljs
Normal file
@ -0,0 +1,261 @@
|
||||
(ns demo.views
|
||||
(:require
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]
|
||||
[demo.events :as events]
|
||||
[demo.routes :as routes]
|
||||
[demo.demo :as demo]
|
||||
[demo.subs :as subs]
|
||||
[demo.merge :as merge]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn read-file
|
||||
[file callback]
|
||||
(let [reader (js/FileReader.)]
|
||||
(set! (.-onload reader)
|
||||
(fn [e]
|
||||
(callback (-> e .-originalTarget .-result))))
|
||||
(.readAsText reader file)))
|
||||
|
||||
(defn download-blob-as-file
|
||||
[blob filename]
|
||||
(let [a (js/document.createElement "a")
|
||||
url (js/URL.createObjectURL blob)]
|
||||
(.setAttribute a "href" url)
|
||||
(.setAttribute a "download" filename)
|
||||
(.click a)))
|
||||
|
||||
(defn merge-files!
|
||||
[files config gen-pdf?]
|
||||
(let [merged-file-contents
|
||||
(merge/merge! files
|
||||
(update config
|
||||
:mapping
|
||||
#(-> %
|
||||
(js/JSON.parse)
|
||||
(js->clj :keywordize-keys true))))
|
||||
merged-data (clj->js merged-file-contents
|
||||
:keyword-fn #(subs (str %) 1))
|
||||
merged-json (js/JSON.stringify merged-data nil 2)]
|
||||
(if-not gen-pdf?
|
||||
(download-blob-as-file
|
||||
(js/Blob. [merged-json]
|
||||
{:type "application/json"}) (or (:output-file-name config) "merge.json"))
|
||||
(let [element (.getElementById js/document "gen-pdf")]
|
||||
(.generatePdf element merged-data)))))
|
||||
|
||||
(defonce dropped-files (r/atom {}))
|
||||
|
||||
(defonce config (r/atom {:output-file-name "merge.json"
|
||||
:document-title "Swagger Document"
|
||||
:document-desc "Swagger Document of API"
|
||||
:document-version "1.0.0"
|
||||
:mapping (js/JSON.stringify
|
||||
(clj->js
|
||||
{"borderRouteDetection.json" {:desc "运维监控 / 运维工具 / 网络诊断"
|
||||
:order 26}
|
||||
"deviceinspection.json" {:desc "运维监控 / 运维工具 / 设备巡检"
|
||||
:order 25}
|
||||
"directconnect.json" {:desc "虚拟网络 / 云专线"
|
||||
:order 11}
|
||||
"elastic.json" {:desc "系统概况 / 概况"
|
||||
:order 1}})
|
||||
nil 2)}))
|
||||
|
||||
(defn handle-upload-file [e]
|
||||
(let [files (.-files (.-target e))]
|
||||
(doseq [file files]
|
||||
(read-file file
|
||||
(fn [contents]
|
||||
(swap! dropped-files assoc (.-name file)
|
||||
(-> (js/JSON.parse contents)
|
||||
(js->clj :keywordize-keys true))))))))
|
||||
|
||||
(defn home-panel []
|
||||
[:<>
|
||||
[:nav.navbar.is-info
|
||||
[:div.container
|
||||
[:div.navbar-brand
|
||||
[:a.navbar-item
|
||||
[:svg {:t "1703483238104"
|
||||
:class "icon"
|
||||
:viewBox "0 0 1024 1024"
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:p-id "4219"
|
||||
:height "200"
|
||||
:width "200"}
|
||||
[:path {:d "M511.18 1018.06C227.739 1016.833 14.337 792.372 6.145 532.686-3.686 224.665 236.339 14.336 490.906 6.144c307.404-9.83 516.505 229.99 526.745 481.485 12.698 306.38-233.472 532.48-506.47 530.432z"
|
||||
:fill "#6D9900"
|
||||
:p-id "4220"}]
|
||||
[:path {:d "M272.589 512c33.997 20.07 44.646 50.586 48.742 85.197 3.072 26.01 1.639 52.019 2.867 78.029 1.844 38.297 12.084 39.321 39.527 39.321 11.878 0 16.384 3.482 14.54 15.155-0.409 1.844 0 3.687 0 5.735 0 33.587 0 33.587-33.791 32.153-47.514-2.048-73.114-29.286-77.62-76.8-3.276-35.43 1.434-71.065-5.12-106.086-5.324-28.672-16.793-39.936-45.875-41.574-7.373-0.41-10.24-2.663-10.035-9.83 0.41-7.169 0-14.132 0-21.3 0.205-9.011-2.048-19.046 1.229-26.624 3.891-8.806 15.36-2.662 23.347-5.12 14.336-4.3 23.552-13.722 27.853-27.648 3.686-11.674 5.939-23.757 6.144-36.045 0.614-32.153-2.048-64.512 4.915-96.051 9.216-42.598 29.082-59.802 72.704-63.693 36.045-3.277 36.045-3.277 36.045 32.563 0 19.661 0 19.866-18.842 20.276-28.877 0.614-32.768 9.42-35.02 36.864-2.049 25.804 0 52.019-2.868 78.028-3.686 35.43-13.926 66.765-48.742 87.45z m478.617 0.205c-38.092-22.733-46.694-58.573-49.356-98.304-1.434-23.143-1.23-46.285-2.458-69.427-1.229-26.624-8.602-33.997-35.226-35.021-19.046-0.615-19.046-0.615-19.046-20.07v-2.868c0-30.925 0-30.925 31.744-30.105 48.947 1.433 74.752 27.238 79.462 75.776 2.663 27.443 1.434 54.886 2.458 82.124 0.41 13.312 2.867 26.215 6.963 38.708 6.554 19.456 16.999 27.443 37.683 27.648 11.06 0 15.36 3.072 14.336 14.54-1.024 12.288-0.614 24.576-0.204 36.864 0.204 7.783-2.458 10.445-10.445 10.855-28.263 1.229-40.755 13.721-45.67 42.189-4.097 23.142-2.663 46.284-2.868 69.222 0 16.998-1.024 33.997-4.71 50.586-9.421 41.37-29.901 58.777-72.09 62.259-36.864 3.072-35.84 2.867-37.273-33.178-0.615-15.36 3.276-19.865 19.046-19.66 27.648 0.204 34.406-7.988 35.635-35.84 1.229-30.72 0.41-61.44 4.915-91.956 5.12-31.13 17.613-57.139 47.104-74.342z"
|
||||
:fill "#FEFEFE"
|
||||
:p-id "4221"}]
|
||||
[:path {:d "M635.7 545.997c-19.252 0.205-35.636-15.565-35.636-33.997 0-17.818 16.18-33.997 34.406-34.202 19.252-0.204 35.636 15.565 35.636 34.202 0 18.022-15.975 33.792-34.407 33.997z m-89.908-34.202c0 20.48-13.926 34.407-34.406 34.407-19.252 0-33.792-14.541-33.792-33.588 0-19.456 15.155-34.61 34.61-34.61 19.252 0 33.588 14.335 33.588 33.791zM389.12 545.997c-20.07 0-34.816-14.746-34.611-34.407 0.205-19.25 14.95-33.587 33.997-33.792 19.046 0 35.225 15.975 35.02 34.816-0.204 18.228-15.77 33.383-34.406 33.383z"
|
||||
:fill "#FEFEFE"
|
||||
:p-id "4222"}]]
|
||||
[:span.ml-2 "SMPG"]]]
|
||||
[:div.navbar-menu
|
||||
[:div.navbar-start
|
||||
[:a.navbar-item {:on-click #(re-frame/dispatch [::events/navigate :home])}
|
||||
"Home"]
|
||||
[:a.navbar-item {:on-click #(js/window.open "https://mrin9.github.io/RapiPdf/" "_blank")}
|
||||
"RapiPdf"]
|
||||
[:a.navbar-item {:on-click #(js/window.open "https://github.com/corkine" "_blank")}
|
||||
[:svg.mr-1 {:t "1703483460437"
|
||||
:class "icon"
|
||||
:viewBox "0 0 1024 1024"
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:p-id "5226"
|
||||
:width 200
|
||||
:height 200}
|
||||
[:path {:d "M850.346667 155.008a42.666667 42.666667 0 0 0-22.741334-23.509333c-8.704-3.754667-85.717333-33.322667-200.32 39.168H396.714667c-114.773333-72.618667-191.701333-42.922667-200.32-39.168a42.88 42.88 0 0 0-22.741334 23.466666c-26.197333 66.218667-18.048 136.448-7.850666 176.896C134.272 374.016 128 413.098667 128 469.333333c0 177.877333 127.104 227.882667 226.730667 246.272a189.568 189.568 0 0 0-13.013334 46.549334A44.373333 44.373333 0 0 0 341.333333 768v38.613333c-19.498667-4.138667-41.002667-11.946667-55.168-26.112C238.08 732.416 188.330667 682.666667 128 682.666667v85.333333c25.002667 0 65.365333 40.362667 97.834667 72.832 51.029333 51.029333 129.066667 55.253333 153.386666 55.253333 3.114667 0 5.376-0.085333 6.528-0.128A42.666667 42.666667 0 0 0 426.666667 853.333333v-82.090666c4.266667-24.746667 20.224-49.621333 27.946666-56.362667a42.666667 42.666667 0 0 0-23.125333-74.581333C293.333333 624.554667 213.333333 591.488 213.333333 469.333333c0-53.12 5.632-70.741333 31.573334-99.285333 11.008-12.117333 14.08-29.568 7.978666-44.8-4.821333-11.904-18.773333-65.450667-6.485333-117.546667 20.650667-1.578667 59.904 4.565333 113.706667 40.96C367.104 253.44 375.466667 256 384 256h256a42.666667 42.666667 0 0 0 23.936-7.338667c54.016-36.522667 92.970667-41.770667 113.664-41.130666 12.330667 52.224-1.578667 105.770667-6.4 117.674666a42.666667 42.666667 0 0 0 8.021333 44.928C805.077333 398.464 810.666667 416.085333 810.666667 469.333333c0 122.581333-79.957333 155.52-218.069334 170.922667a42.666667 42.666667 0 0 0-23.125333 74.709333c19.797333 17.066667 27.861333 32.469333 27.861333 53.034667v128h85.333334v-128c0-20.437333-3.925333-38.101333-9.770667-53.12C769.92 695.765333 896 643.712 896 469.333333c0-56.362667-6.272-95.530667-37.76-137.514666 10.197333-40.405333 18.261333-110.506667-7.893333-176.810667z"
|
||||
:fill "#ffffff"
|
||||
:p-id "5227"}]]
|
||||
"About"]]]]]
|
||||
[:section.hero.is-info
|
||||
[:div.hero-body
|
||||
[:div.container
|
||||
[:h1.title
|
||||
[:span "Swagger Merge and PDF Generator"]]
|
||||
[:h2.subtitle
|
||||
[:span "Concat multiple swagger specs into one, and generate PDF for it!"]]]]]
|
||||
[:div.container.content.mt-5
|
||||
[:div.ml-5
|
||||
[:div.file.is-boxed.mt-3
|
||||
[:label.file-label
|
||||
[:input.file-input {:type "file"
|
||||
:accept ".json"
|
||||
:multiple :multiple
|
||||
:on-change handle-upload-file}]
|
||||
[:span.file-cta
|
||||
[:span.file-label "Read Swagger .json file(s)"]]]]
|
||||
[:div.mt-3
|
||||
(if (empty? @dropped-files)
|
||||
[:div "No files uploaded"]
|
||||
[:div
|
||||
[:div
|
||||
(for [[k v] @dropped-files]
|
||||
^{:key k}
|
||||
[:div.mb-2
|
||||
[:div
|
||||
[:strong k]]
|
||||
[:div
|
||||
[:pre (pr-str v)]]])]])]
|
||||
[:div.mt-5.mb-5
|
||||
[:h4 "Config Settings"]
|
||||
[:div.field.is-horizontal
|
||||
[:div.field-label.is-normal
|
||||
[:label.label "Output File Name"]]
|
||||
[:div.field-body
|
||||
[:div.field
|
||||
[:p.control
|
||||
[:input.input {:type "text"
|
||||
:on-change (fn [e]
|
||||
(let [data (-> e .-target .-value)]
|
||||
(when-not (or (nil? data) (str/blank? data))
|
||||
(swap! config assoc :output-file-name data))))
|
||||
:placeholder "Output File Name"
|
||||
:default-value (:output-file-name @config)}]]]]]
|
||||
[:div.field.is-horizontal
|
||||
[:div.field-label.is-normal
|
||||
[:label.label "Document Title"]]
|
||||
[:div.field-body
|
||||
[:div.field
|
||||
[:p.control
|
||||
[:input.input {:type "text"
|
||||
:on-change (fn [e]
|
||||
(let [data (-> e .-target .-value)]
|
||||
(when-not (or (nil? data) (str/blank? data))
|
||||
(swap! config assoc :document-title data))))
|
||||
:placeholder "Document Title"
|
||||
:default-value (:document-title @config)}]]]]]
|
||||
[:div.field.is-horizontal
|
||||
[:div.field-label.is-normal
|
||||
[:label.label "Document Desc"]]
|
||||
[:div.field-body
|
||||
[:div.field
|
||||
[:p.control
|
||||
[:input.input {:type "text"
|
||||
:on-change (fn [e]
|
||||
(let [data (-> e .-target .-value)]
|
||||
(when-not (or (nil? data) (str/blank? data))
|
||||
(swap! config assoc :document-desc data))))
|
||||
:placeholder "Document Description"
|
||||
:default-value (:document-desc @config)}]]]]]
|
||||
[:div.field.is-horizontal
|
||||
[:div.field-label.is-normal
|
||||
[:label.label "Document Version"]]
|
||||
[:div.field-body
|
||||
[:div.field
|
||||
[:p.control
|
||||
[:input.input {:type "text"
|
||||
:on-change (fn [e]
|
||||
(let [data (-> e .-target .-value)]
|
||||
(when-not (or (nil? data) (str/blank? data))
|
||||
(swap! config assoc :document-version data))))
|
||||
:placeholder "Document Version"
|
||||
:default-value (:document-version @config)}]]]]]
|
||||
[:div.field.is-horizontal
|
||||
[:div.field-label.is-normal
|
||||
[:label.label "File2TagName Map"]]
|
||||
[:div.field-body
|
||||
[:div.field
|
||||
[:p.control
|
||||
[:textarea.textarea
|
||||
{:placeholder "Mapping FileName to TagName Prefix, and set it's order"
|
||||
:rows 10
|
||||
:on-change (fn [e]
|
||||
(let [data (-> e .-target .-value)]
|
||||
(if (or (nil? data) (str/blank? data))
|
||||
(swap! config assoc :mapping {})
|
||||
(try
|
||||
(swap! config assoc :mapping (js/JSON.parse data))
|
||||
(catch js/Error _ (println "Invalid JSON format!"))))))
|
||||
:default-value (:mapping @config)}]]]]]
|
||||
[:div.field.is-horizontal
|
||||
[:div.field-label.is-normal]
|
||||
[:div.field-body
|
||||
[:div.field
|
||||
[:article.message.is-warning
|
||||
[:div.message-body
|
||||
"Add a prefix to and sort the tags of a specific .json file。e.g. if a.json sets desc to \"A\", then all the tags in its file become \"A / Tag\", and the tags will be sorted by the order field after the files are merged."]]]]]]
|
||||
[:div.mt-5.mb-5.is-flex.is-justify-content-flex-end
|
||||
(when (empty? @dropped-files)
|
||||
[:div
|
||||
[:button.button.is-info
|
||||
{:on-click #(merge-files! (demo/try-with-demo) @config true)}
|
||||
"Try with Demo Swagger Spec!"]])
|
||||
(when (not-empty @dropped-files)
|
||||
[:button.button.is-danger
|
||||
{:on-click #(reset! dropped-files {})}
|
||||
"Clean Read Files"])
|
||||
(when (not-empty @dropped-files)
|
||||
[:button.button.is-info.ml-2
|
||||
{:on-click (fn []
|
||||
(println @config)
|
||||
(merge-files! @dropped-files @config false))}
|
||||
"Download Merged .json File"])
|
||||
(when (not-empty @dropped-files)
|
||||
[:button.button.is-info.ml-2
|
||||
{:on-click (fn []
|
||||
(println @config)
|
||||
(merge-files! @dropped-files @config true))}
|
||||
"Merge and Generated PDF!"])]]]])
|
||||
|
||||
(defmethod routes/panels :home-panel [] [home-panel])
|
||||
|
||||
(defn about-panel []
|
||||
[:div
|
||||
[:h1 "This is the About Page."]
|
||||
|
||||
[:div
|
||||
[:a {:on-click #(re-frame/dispatch [::events/navigate :home])}
|
||||
"go to Home Page"]]])
|
||||
|
||||
(defmethod routes/panels :about-panel [] [about-panel])
|
||||
|
||||
(defn main-panel []
|
||||
(let [active-panel (re-frame/subscribe [::subs/active-panel])]
|
||||
(routes/panels @active-panel)))
|
Reference in New Issue
Block a user