├── .dockerignore
├── .editorconfig
├── .github
└── workflows
│ ├── main.yaml
│ ├── objectscript-quality.yml
│ └── zpm.yml
├── .gitignore
├── .vscode
└── settings.json
├── Dockerfile
├── Dockerfile.iris
├── LICENSE
├── README.md
├── docker-compose.cache.yml
├── docker-compose.iris.yml
├── docker-compose.yml
├── entrypoint.sh
├── images
├── ENSLIB_5_1_0.png
├── IRISSYS_5_1_0.png
├── IRISSYS_5_1_1.png
├── MapView.png
├── TESTDB_20_1_0.png
├── TESTDB_20_1_1.png
└── TreeView.png
├── makefile
├── module.xml
├── server
├── .dockerignore
├── Dockerfile
├── Dockerfile.iris
└── src
│ ├── Blocks
│ ├── BlocksInstaller.cls
│ ├── BlocksMap.cls
│ ├── Router.cls
│ ├── StandaloneInstaller.cls
│ └── WebSocket.cls
│ └── DevInstaller.cls
└── web
├── .babelrc
├── .eslintrc.json
├── Dockerfile
├── index.html
├── js
├── blocksViewer.js
├── joint.shapes.blocks.js
├── main.js
├── mapViewer.js
└── wsEventDispatcher.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── styles
└── main.scss
└── webpack.config.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.*
2 | **/node_modules
3 | build
4 | *.log
5 | *.md
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.cls]
13 | indent_size = 2
14 | indent_style = space
15 | insert_final_newline = false
16 |
17 | [*.md]
18 | insert_final_newline = false
19 | trim_trailing_whitespace = false
20 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | - release
4 |
5 | jobs:
6 | build:
7 | strategy:
8 | matrix:
9 | variant: [
10 | "cache",
11 | "iris"
12 | ]
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@master
16 | - name: Login to Docker Registry
17 | run: docker login --username $DOCKER_USERNAME --password $DOCKER_PASSWORD
18 | env:
19 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
20 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
21 | - name: Build and push
22 | run: make ${{ matrix.variant }}
23 |
--------------------------------------------------------------------------------
/.github/workflows/objectscript-quality.yml:
--------------------------------------------------------------------------------
1 | name: objectscriptquality
2 | on: push
3 |
4 | jobs:
5 | linux:
6 | name: Linux build
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - name: Execute ObjectScript Quality Analysis
11 | run: wget https://raw.githubusercontent.com/litesolutions/objectscriptquality-jenkins-integration/master/iris-community-hook.sh && sh ./iris-community-hook.sh
12 |
13 |
--------------------------------------------------------------------------------
/.github/workflows/zpm.yml:
--------------------------------------------------------------------------------
1 | name: zpm
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | - uses: actions/setup-node@v3
12 | with:
13 | node-version: '10.x'
14 | - run: cd web && npm ci
15 | - run: cd web && npm run build:prod
16 | - name: zpm
17 | uses: isc-zpm/actions@master
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | /temp/
4 | build
5 | out
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "objectscript.conn": {
3 | "active": true,
4 | "ns": "BLOCKS",
5 | "docker-compose": {
6 | "service": "server"
7 | },
8 | "links": {
9 | "BlocksExplorer": "http://localhost/"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG CACHE_VERSION=2018.1
2 | FROM node:8-alpine AS web
3 |
4 | WORKDIR /opt/blocks/
5 |
6 | COPY web ./
7 |
8 | RUN npm install \
9 | && export PATH="$PATH:./node_modules/.bin" \
10 | && webpack --mode production
11 |
12 | FROM daimor/intersystems-cache:${CACHE_VERSION}
13 |
14 | WORKDIR /opt/blocks
15 |
16 | RUN yum -y install ImageMagick
17 |
18 | COPY ./server/src/ ./src
19 | COPY --from=web /opt/blocks/build/ /usr/cachesys/csp/blocks/
20 |
21 | RUN ccontrol start $ISC_PACKAGE_INSTANCENAME quietly \
22 | && echo -e "" \
23 | "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \
24 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \
25 | "do:'sc \$zu(4,\$j,1)\n" \
26 | "halt\n" \
27 | | csession $ISC_PACKAGE_INSTANCENAME -UUSER \
28 | # Stop Caché instance
29 | && ccontrol stop $ISC_PACKAGE_INSTANCENAME quietly
30 |
31 | VOLUME [ "/opt/blocks/db" ]
32 |
33 | COPY entrypoint.sh /
34 |
35 | ENTRYPOINT [ "/entrypoint.sh" ]
36 |
--------------------------------------------------------------------------------
/Dockerfile.iris:
--------------------------------------------------------------------------------
1 | FROM node:8-alpine AS web
2 |
3 | WORKDIR /opt/blocks/
4 |
5 | COPY web ./
6 |
7 | RUN npm install \
8 | && export PATH="$PATH:./node_modules/.bin" \
9 | && webpack --mode production
10 |
11 | FROM intersystemsdc/iris-community
12 |
13 | WORKDIR /opt/blocks
14 |
15 | USER root
16 |
17 | RUN chown ${ISC_PACKAGE_MGRUSER}:${ISC_PACKAGE_IRISGROUP} ${PWD} && \
18 | apt-get -y update && \
19 | DEBIAN_FRONTEND=noninteractive apt-get -y install imagemagick --no-install-recommends && \
20 | rm -rf /var/lib/apt/lists/*
21 |
22 | USER irisowner
23 |
24 | COPY ./server/src/ ./src
25 | COPY --from=web /opt/blocks/build/ /usr/irissys/csp/blocks/
26 |
27 | RUN iris start $ISC_PACKAGE_INSTANCENAME quietly \
28 | && /bin/echo -e "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \
29 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \
30 | "do:'sc \$zu(4,\$j,1)\n" \
31 | "Halt\n" \
32 | | iris session ${ISC_PACKAGE_INSTANCENAME} \
33 | && iris stop $ISC_PACKAGE_INSTANCENAME quietly
34 |
35 | VOLUME [ "/opt/blocks/db" ]
36 |
37 | COPY entrypoint.sh /
38 |
39 | ENTRYPOINT [ "/entrypoint.sh" ]
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2019 Dmitry Maslennikov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Blocks Explorer
4 | [](https://community.objectscriptquality.com/dashboard?id=intersystems_iris_community%2FCacheBlocksExplorer)
5 |
6 | Database Blocks Explorer for InterSystems IRIS/Caché
7 |
8 | #### Key features
9 | ##### Tree explorer
10 | + Shows tree of database blocks;
11 | + Export tree as SVG or PNG image;
12 | + Shows every node in the block;
13 | + Open any block just by clicking on node in parent block;
14 | + Reload block info by clicking at the same node second time;
15 | + Zoom in and out, fit and navigator;
16 | + Easy way to switch between view modes (tree/map);
17 |
18 | ##### Fragmentation map
19 | + Shows every block with the same colour for every globals;
20 | + Legend for globals;
21 |
22 | #### Run with Docker
23 | You need license key for Caché or IRIS on RedHat systems.
24 | ##### Caché
25 | ```
26 | docker run -d --name blocksexplorer --rm \
27 | -p 57772:57772 \
28 | -v /opt/some/database/for/test:/opt/blocks/db/test \
29 | -v ~/cache.key:/usr/cachesys/mgr/cache.key \
30 | daimor/blocksexplorer:cache
31 | ```
32 | Generate blocks map as image file in `out` directory
33 | ```
34 | docker run -it --rm \
35 | -v /opt/some/database/for/test:/opt/blocks/db/test \
36 | -v `pwd`/out:/opt/blocks/out \
37 | daimor/blocksexplorer:cache generate
38 | ```
39 |
40 | ##### IRIS
41 | ```
42 | docker run -d --name blocksexplorer --rm \
43 | -p 52773:52773 \
44 | -v /opt/some/database/for/test:/opt/blocks/db/test \
45 | daimor/blocksexplorer:iris
46 | ```
47 |
48 | Generate blocks map as image file in `out` directory
49 | ```
50 | docker run -it --rm \
51 | -v /opt/some/database/for/test:/opt/blocks/db/test \
52 | -v `pwd`/out:/opt/blocks/out \
53 | daimor/blocksexplorer:iris generate
54 | ```
55 |
56 |
57 | #### Development mode
58 | Run with docker-compose, will start web part with hot reloading.
59 | ```
60 | docker-compose up -d --build
61 | ```
62 | It will start server base on IRIS
63 | To start on Caché use this command
64 | ```
65 | MODE=cache docker-compose up -d --build
66 | ```
67 | By default running on 80 port. To start using it, just open http://localhost/
68 |
69 | ## Screenshots
70 |
71 | 
72 | 
73 |
74 | ## CLI mode
75 |
76 | Using prebuild docker image gives a way to generate a picture for any IRIS or Caché database.
77 | Use docker image `daimor/blocksexplorer:iris` for IRIS or `daimor/blocksexplorer:cache` for Caché Databases.
78 | Those images accepts command `generate` with arguments
79 |
80 | * path to the tested databases inside a container, by default `/db`, can be omited
81 | * cellSize - size of the cell in pixels, where each cell represents particular database's block, by default 1
82 | * cellSpace - sorrounding space between cell, by default 0
83 | * showFill - sign to show how much block fill by data, by default 0
84 |
85 | This tool generates a square picture in folder /out inside a container in formats BMP and PNG.
86 |
87 | So, with command like this
88 | ```
89 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate 20 1 1
90 | ```
91 | It will generate this picture for an empty database.
92 |
93 | 
94 | With a lighter color visible that most of the blocks just empty.
95 |
96 | The same test empty database, but with showFill=0
97 | ```
98 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate 20 1 0
99 | ```
100 | Blocks have different colors but just for globals, and does not show how much it fill.
101 |
102 | 
103 |
104 | More examples
105 | ENSLIB
106 | ```
107 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate /usr/irissys/mgr/enslib 5 1
108 | ```
109 | 
110 |
111 | IRISSYS
112 | ```
113 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate /usr/irissys/mgr/ 5 1 1
114 | ```
115 | 
116 |
117 |
118 | For large databases, would not recommend to use have too big cellSize.
119 |
120 | ### Useful Links
121 |
122 | There you can find more about database internals, and how to use this tool.
123 | * [Internal Structure of Caché Database Blocks, Part 1](https://community.intersystems.com/post/internal-structure-cach%C3%A9-database-blocks-part-1)
124 | * [Internal Structure of Caché Database Blocks, Part 2](https://community.intersystems.com/post/internal-structure-cach%C3%A9-database-blocks-part-2)
125 | * [Internal Structure of Caché Database Blocks, Part 3](https://community.intersystems.com/post/internal-structure-cach%C3%A9-database-blocks-part-3)
126 |
127 |
--------------------------------------------------------------------------------
/docker-compose.cache.yml:
--------------------------------------------------------------------------------
1 | version: "2.4"
2 |
3 | services:
4 | server:
5 | build:
6 | context: server
7 | args:
8 | CACHE_VERSION: ${CACHE_VERSION-2018.1}
9 | ports:
10 | - 57772
11 | - 1972
12 | volumes:
13 | - ~/cache.key:/usr/cachesys/mgr/cache.key
14 | - ./server/src:/opt/blocks/src
15 | web:
16 | environment:
17 | - DB_PORT=57772
18 |
--------------------------------------------------------------------------------
/docker-compose.iris.yml:
--------------------------------------------------------------------------------
1 | version: "2.4"
2 |
3 | services:
4 | server:
5 | build:
6 | context: server
7 | dockerfile: Dockerfile.iris
8 | command: --check-caps false
9 | ports:
10 | - 1972
11 | - 52773
12 | volumes:
13 | # - ~/iris.key:/usr/irissys/mgr/iris.key
14 | - ./server/src:/opt/blocks/src
15 | web:
16 | environment:
17 | - DB_PORT=52773
18 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "2.4"
2 |
3 | services:
4 | server:
5 | extends:
6 | file: docker-compose.${MODE:-iris}.yml
7 | service: server
8 | volumes:
9 | - ./out:/opt/blocks/out
10 | web:
11 | extends:
12 | file: docker-compose.${MODE:-iris}.yml
13 | service: web
14 | build:
15 | context: web
16 | environment:
17 | - DB_HOST=server
18 | - WEB_PORT=${WEB_PORT-80}
19 | ports:
20 | - "${WEB_PORT-80}:${WEB_PORT-80}"
21 | volumes:
22 | - node_modules:/opt/app/node_modules
23 | - web_build:/opt/app/build
24 | - ./web:/opt/app
25 | volumes:
26 | node_modules: null
27 | web_build: null
28 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ "$#" -lt 1 ]; then
4 | # just start server
5 | if [ -f /iris-main ]; then
6 | /iris-main
7 | else
8 | /ccontainermain
9 | fi
10 | exit
11 | fi
12 |
13 | COMMAND=$1
14 | if [ -x "$(command -v ccontrol)" ]; then
15 | CCONTROL=ccontrol
16 | else
17 | CCONTROL=iris
18 | fi
19 |
20 | if [ "${COMMAND,,}" = "generate" ]; then
21 | shift
22 | re='^[0-9]+$'
23 | if ! [[ "$1" =~ $re ]]; then
24 | DATABASE=$1
25 | shift
26 | fi
27 | OutputFolder=/opt/blocks/out/
28 | CellSize=${1:-1}
29 | CellSpace=${2:-0}
30 | ShowFill=${3:-0}
31 | DATABASE=${DATABASE:-/db}
32 |
33 | echo
34 | echo "Starting server..."
35 | $CCONTROL start $ISC_PACKAGE_INSTANCENAME quietly
36 | echo
37 | echo "Generating image..."
38 | echo "Database = \"$DATABASE\""
39 | echo "OutputFolder = \"$OutputFolder\""
40 | echo "CellSize = $CellSize"
41 | echo "CellSpace = $CellSpace"
42 | echo "ShowFill = $ShowFill"
43 | rm ${OutputFolder}BlocksMap.{png,bmp}
44 | $CCONTROL session $ISC_PACKAGE_INSTANCENAME -UBLOCKS "##class(Blocks.BlocksMap).Generate(\"$DATABASE\",\"${OutputFolder}\",\"${CellSize}\",\"${CellSpace}\",\"${ShowFill}\")"
45 | echo
46 | echo "Stopping server..."
47 | $CCONTROL stop $ISC_PACKAGE_INSTANCENAME quietly
48 | echo "Finished"
49 | else
50 | /bin/echo -e "" \
51 | "Available commands:\n\n" \
52 | " help - this help\n" \
53 | " generate - will generate BlocksMap for the database located in /opt/blocks/db/test/\n" \
54 | " as an image in bmp and png format in folder /opt/blocks/out\n"
55 | " \n"
56 | fi
57 |
--------------------------------------------------------------------------------
/images/ENSLIB_5_1_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/ENSLIB_5_1_0.png
--------------------------------------------------------------------------------
/images/IRISSYS_5_1_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/IRISSYS_5_1_0.png
--------------------------------------------------------------------------------
/images/IRISSYS_5_1_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/IRISSYS_5_1_1.png
--------------------------------------------------------------------------------
/images/MapView.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/MapView.png
--------------------------------------------------------------------------------
/images/TESTDB_20_1_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/TESTDB_20_1_0.png
--------------------------------------------------------------------------------
/images/TESTDB_20_1_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/TESTDB_20_1_1.png
--------------------------------------------------------------------------------
/images/TreeView.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/TreeView.png
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | IMAGE=daimor/blocksexplorer
2 |
3 | .PHONY: clean cache iris web
4 |
5 | build: clean web
6 |
7 | web:
8 | cd web && npm ci && npm run build:prod
9 |
10 | clean:
11 | rm -rf web/build
12 |
13 | cache:
14 | docker build -t $(IMAGE):cache .
15 | docker push $(IMAGE):cache
16 |
17 | iris:
18 | docker build -f Dockerfile.iris -t $(IMAGE):iris .
19 | docker push $(IMAGE):iris
20 |
--------------------------------------------------------------------------------
/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | BlocksExplorer
5 | 2.2.1
6 | module
7 | server/src
8 |
9 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.*
2 | .*
3 |
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG CACHE_VERSION=2017.2
2 | FROM daimor/intersystems-cache:${CACHE_VERSION}
3 |
4 | WORKDIR /opt/blocks
5 |
6 | COPY ./src/ ./src
7 |
8 | RUN ccontrol start $ISC_PACKAGE_INSTANCENAME quietly \
9 | && echo -e "" \
10 | "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \
11 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \
12 | "do:'sc \$zu(4,\$j,1)\n" \
13 | "halt\n" \
14 | | csession $ISC_PACKAGE_INSTANCENAME -UUSER \
15 | # Stop Caché instance
16 | && ccontrol stop $ISC_PACKAGE_INSTANCENAME quietly
17 |
18 | VOLUME [ "/opt/blocks/db" ]
19 |
--------------------------------------------------------------------------------
/server/Dockerfile.iris:
--------------------------------------------------------------------------------
1 | # ARG IRIS_VERSION=2020.1.0.202.0
2 | # FROM store/intersystems/iris-community:${IRIS_VERSION}
3 | FROM intersystemsdc/iris-community
4 |
5 | WORKDIR /opt/blocks
6 |
7 | USER root
8 |
9 | RUN chown ${ISC_PACKAGE_MGRUSER}:${ISC_PACKAGE_IRISGROUP} ${PWD} && \
10 | apt-get -y update && \
11 | DEBIAN_FRONTEND=noninteractive apt-get -y install imagemagick --no-install-recommends && \
12 | rm -rf /var/lib/apt/lists/*
13 |
14 | USER irisowner
15 |
16 | COPY ./src/ ./src
17 |
18 | RUN iris start $ISC_PACKAGE_INSTANCENAME quietly && \
19 | /bin/echo -e "" \
20 | "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \
21 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \
22 | "do:'sc \$zu(4,\$j,1)\n" \
23 | "do INT^JRNSTOP kill ^%SYS(\"Journal\")\n" \
24 | "do ##class(Security.Users).UnExpireUserPasswords(\"*\")\n" \
25 | "halt" \
26 | | iris session $ISC_PACKAGE_INSTANCENAME -U%SYS && \
27 | iris stop $ISC_PACKAGE_INSTANCENAME quietly && \
28 | rm $ISC_PACKAGE_INSTALLDIR/mgr/IRIS.WIJ && \
29 | rm $ISC_PACKAGE_INSTALLDIR/mgr/journal/*
30 |
31 | VOLUME [ "/opt/blocks/db" ]
32 |
33 |
--------------------------------------------------------------------------------
/server/src/Blocks/BlocksInstaller.cls:
--------------------------------------------------------------------------------
1 | Class Blocks.BlocksInstaller Extends %Projection.AbstractProjection
2 | {
3 |
4 | Projection Reference As BlocksInstaller;
5 |
6 | Parameter CSPAPP As %String = "/blocks";
7 |
8 | Parameter CSPAPPDESCRIPTION As %String = "A WEB application for Cache Blocks Explorer.";
9 |
10 | Parameter ROUTER As %String = "Blocks.Router";
11 |
12 | /// This method is invoked when a class is compiled.
13 | ClassMethod CreateProjection(cls As %String, ByRef params) As %Status
14 | {
15 | set ns=$namespace
16 | new $namespace
17 | znspace "%SYS"
18 |
19 | if ('##class(Security.Applications).Exists(..#CSPAPP)) {
20 | do ##class(Security.System).GetInstallationSecuritySetting(.security)
21 | set cspProperties("AutheEnabled") = $select((security="None"):64,1:32)
22 | set cspProperties("NameSpace") = ns
23 | set cspProperties("Description") = ..#CSPAPPDESCRIPTION
24 | set cspProperties("DispatchClass") = ..#ROUTER
25 | write !, "Creating WEB application """_..#CSPAPP_"""..."
26 | $$$ThrowOnError(##class(Security.Applications).Create(..#CSPAPP, .cspProperties))
27 | write !, "WEB application """_..#CSPAPP_""" created."
28 | if ##class(%Studio.General).GetWebServerPort(,,,.url) {
29 | write !, "You can now open it with a link: "_url_$p(..#CSPAPP,"/",2,*)_"/"
30 | }
31 | } else {
32 | write !, "WEB application """_..#CSPAPP_""" already exists, so it is ready to use."
33 | }
34 | Quit $$$OK
35 | }
36 |
37 | /// This method is invoked when a class is 'uncompiled'.
38 | ClassMethod RemoveProjection(cls As %String, ByRef params, recompile As %Boolean) As %Status
39 | {
40 | new $namespace
41 | znspace "%SYS"
42 |
43 | if (##class(Security.Applications).Exists(..#CSPAPP)) {
44 | w !, "Deleting WEB application """_..#CSPAPP_"""..."
45 | do ##class(Security.Applications).Delete(..#CSPAPP)
46 | w !, "WEB application """_..#CSPAPP_""" was successfully removed."
47 | }
48 | QUIT $$$OK
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/server/src/Blocks/BlocksMap.cls:
--------------------------------------------------------------------------------
1 | Class Blocks.BlocksMap
2 | {
3 |
4 | Parameter DefaultOutputFolder = "/opt/blocks/out/";
5 |
6 | ClassMethod Generate(aDirectory = "", pOutputFolder = {..#DefaultOutputFolder}, cellSize = 1, cellSpace = 0, showFill = 0) As %Status
7 | {
8 | If '##class(%File).DirectoryExists(pOutputFolder) {
9 | Quit $$$ERROR($$$GeneralError, "OutputFolder must exist")
10 | }
11 |
12 | Set tSC = ..GenerateMap(aDirectory)
13 | If $$$ISERR(tSC) {
14 | Do $System.OBJ.DisplayError(tSC)
15 | Quit tSC
16 | }
17 |
18 | Set tSC = ..DrawMap(.pOutputFolder, cellSize, cellSpace, showFill)
19 | If $$$ISERR(tSC) {
20 | Do $System.OBJ.DisplayError(tSC)
21 | Quit tSC
22 | }
23 |
24 | Set tSC = ..ConvertMap(.pOutputFolder)
25 | If $$$ISERR(tSC) {
26 | Do $System.OBJ.DisplayError(tSC)
27 | Quit tSC
28 | }
29 | Quit $$$OK
30 | }
31 |
32 | ClassMethod ConvertMap(pOutputFolder = {..#DefaultOutputFolder}) As %Status
33 | {
34 | Set fileNameBMP = ##class(%File).NormalizeFilename("BlocksMap.bmp", pOutputFolder)
35 | Set fileNamePNG = ##class(%File).NormalizeFilename("BlocksMap.png", pOutputFolder)
36 | Do ##class(%File).Delete(fileNamePNG)
37 |
38 | Set ok = $ZF(-100, "/SHELL", "convert", fileNameBMP, fileNamePNG)
39 | Quit $$$OK
40 | }
41 |
42 | ClassMethod DrawMap(pOutputFolder = {..#DefaultOutputFolder}, cellSize = 1, cellSpace = 0, showFill = 0) As %Status
43 | {
44 | Set mapGN = $Name(^||BlocksMap)
45 | Do ..initColors()
46 | Set cellSpace = $System.SQL.CEILING(cellSpace / 2) * 2
47 |
48 | Set fileName = ##class(%File).NormalizeFilename("BlocksMap.bmp", pOutputFolder)
49 | Do ##class(%File).Delete(fileName)
50 |
51 | Set file = ##class(%Stream.FileBinary).%New()
52 | Set file.Filename = fileName
53 | Do file.Clear()
54 |
55 | Set $Listbuild(blocks, size) = $Get(@mapGN)
56 |
57 | Set width = size * ( cellSize + cellSpace )
58 | Set height = width
59 |
60 | Set bytesPerPixel = 3
61 | Set bfOffBits = 54
62 |
63 | Set bfSize = bfOffBits + (bytesPerPixel*width*height)
64 | Set fileHeader = ""
65 | _ "BM" // WORD bfType
66 | _ ..justify(bfSize, 4) // DWORD bfSize
67 | _ ..justify(0, 2) // WORD bfReserved1
68 | _ ..justify(0, 2) // WORD bfReserved2
69 | _ ..justify(bfOffBits, 4) // DWORD bfOffBits
70 |
71 | Set biSize = 40
72 | Set biWidth = width
73 | Set biHeight = height
74 | Set biPlanes = 1
75 | Set biBitCount = bytesPerPixel * 8
76 | Set biCompression = 0
77 | Set biSizeImage = 0
78 | Set biXPelsPerMeter = 0
79 | Set biYPelsPerMeter = 0
80 | Set biClrUsed = 0
81 | Set biClrImportant = 0
82 |
83 | Set infoHeader = ""
84 | _ ..justify(biSize, 4) // DWORD biSize;
85 | _ ..justify(biWidth, 4) // LONG biWidth;
86 | _ ..justify(biHeight, 4) // LONG biHeight;
87 | _ ..justify(biPlanes, 2) // WORD biPlanes;
88 | _ ..justify(biBitCount, 2) // WORD biBitCount;
89 | _ ..justify(biCompression, 4) // DWORD biCompression;
90 | _ ..justify(biSizeImage, 4) // DWORD biSizeImage;
91 | _ ..justify(biXPelsPerMeter, 4) // LONG biXPelsPerMeter;
92 | _ ..justify(biYPelsPerMeter, 4) // LONG biYPelsPerMeter;
93 | _ ..justify(biClrUsed, 4) // DWORD biClrUsed;
94 | _ ..justify(biClrImportant, 4) // DWORD biClrImportant;
95 |
96 |
97 | Do file.Write(fileHeader)
98 | Do file.Write(infoHeader)
99 |
100 | Set paddingSize = (4 - (width * bytesPerPixel) # 4) # 4
101 | For y=biHeight:-1:1 {
102 | Set line = ""
103 | For x=1:1:biWidth {
104 | Set $Listbuild(red, green, blue) = ..getColor(y, x, size, cellSize, cellSpace, showFill)
105 | Set color = ""
106 | _ ..justify(red, 1)
107 | _ ..justify(green, 1)
108 | _ ..justify(blue, 1)
109 | Set line = line _ color
110 | }
111 | Do file.Write(line _ ..justify(0, paddingSize))
112 | }
113 |
114 | Set tSC = file.%Save()
115 | Do file.%Close()
116 | Quit tSC
117 | }
118 |
119 | ClassMethod initColors() As %Status
120 | {
121 | Set mapGN = $Name(^||BlocksMap)
122 | Set maxCount = $Get(@mapGN@("Globals"))
123 | Kill @mapGN@("Colors")
124 |
125 | For i=1:1:maxCount {
126 | Set ksi = i / maxCount
127 |
128 | If (ksi < 0.5) {
129 | Set red = ksi * 2
130 | Set blue = (0.5 - ksi) * 2
131 | }
132 | Else {
133 | Set red = (1.0 - ksi) * 2
134 | Set blue = (ksi - 0.5) * 2
135 | }
136 |
137 | If (ksi >= 0.3) && (ksi < 0.8) {
138 | Set green = (ksi - 0.3) * 2
139 | }
140 | ElseIf (ksi < 0.3) {
141 | Set green = (0.3 - ksi) * 2
142 | }
143 | Else {
144 | Set green = (1.3 - ksi) * 2
145 | }
146 |
147 | Set red = $System.SQL.FLOOR(red * 255)
148 | Set green = $System.SQL.FLOOR(green * 255)
149 | Set blue = $System.SQL.FLOOR(blue * 255)
150 |
151 | Set color = $Listbuild(red, green, blue)
152 | Set @mapGN@("Colors", i) = color
153 | }
154 | Quit $$$OK
155 | }
156 |
157 | ClassMethod getColor(y, x, size, cellSize, cellSpace, showFill) As %List
158 | {
159 | Set mapGN = $Name(^||BlocksMap)
160 |
161 | Set padding = 0
162 | Set spacing = 0
163 | If cellSpace {
164 | Set padding = cellSpace \ 2
165 |
166 | Set spacing = (y <= padding) || (x <= padding)
167 | Set spacing = spacing || (x # (cellSize + cellSpace) <= padding)
168 | Set spacing = spacing || (y # (cellSize + cellSpace) <= padding)
169 | }
170 | Set blockX = x - 1 \ (cellSize + cellSpace) + 1
171 | Set blockY = y - 1 \ (cellSize + cellSpace) + 1
172 |
173 | If spacing {
174 | Set red = 222
175 | Set green = 222
176 | Set blue = 222
177 | }
178 | ElseIf $Data(@mapGN@("Map", blockY, blockX), blockInfo) {
179 | Set $Listbuild(globalNum, fill) = blockInfo
180 | Set $Listbuild(red, green, blue) = $Get(@mapGN@("Colors", globalNum))
181 | if (showFill) {
182 | Set fillSize = $System.SQL.CEILING(fill / 100 * cellSize) + 1
183 | Set:fillSize<1 fillSize = 1
184 | if ( y # (cellSize + cellSpace) > fillSize) {
185 | Set alpha = 0.5
186 | Set red = (1-alpha) * 255 + (alpha * red)
187 | Set green = (1-alpha) * 255 + (alpha * green)
188 | Set blue = (1-alpha) * 255 + (alpha * blue)
189 | }
190 | }
191 | }
192 | Else {
193 | Set red = 255
194 | Set green = 255
195 | Set blue = 255
196 | }
197 |
198 | Quit $Listbuild(red, green, blue)
199 | }
200 |
201 | ClassMethod GenerateMap(aDirectory = "") As %Status
202 | {
203 | #Define SYS do ##Continue
204 | . New $Namespace Set $Namespace = "%SYS"
205 |
206 | If aDirectory="" {
207 | Quit $$$ERROR($$$GeneralError, "Directory must be specified")
208 | }
209 | $$$SYS Set db = ##class(SYS.Database).%OpenId(aDirectory)
210 | If '$IsObject(db) {
211 | Quit $$$ERROR($$$GeneralError, "Specified database does not exists or does not available")
212 | }
213 |
214 | Set mapGN = $Name(^||BlocksMap)
215 | Kill @mapGN
216 |
217 | Set blocks = db.Blocks
218 | Set size = $System.SQL.CEILING($ZSqr(blocks))
219 | Set @mapGN = $Listbuild(blocks, size)
220 |
221 | Set resName = "blocks"_$Job
222 | If '$System.Event.Defined(resName) {
223 | Do $System.Event.Create(resName)
224 | }
225 |
226 | Job ##class(Blocks.WebSocket).GetBlocks(aDirectory, 0, resName)
227 |
228 | Set $Listbuild(sc, data)=$System.Event.WaitMsg(resName, 0)
229 | Set child = $ZChild
230 |
231 | Set atEnd = 0
232 | While 'atEnd {
233 | Set $Listbuild(sc,data)=$System.Event.WaitMsg(resName, 0)
234 | If sc<=0,'$Data(^$JOB(child)) {
235 | Set atEnd = 1
236 | Quit
237 | }
238 | Continue:data=""
239 | Set $Listbuild(parent, block, global, fill) = data
240 | Continue:global=""
241 | If '$Data(@mapGN@("Globals", global), globalNum) {
242 | Set globalNum = $Increment(@mapGN@("Globals"))
243 | Set @mapGN@("Globals", global) = globalNum
244 | }
245 |
246 | Set x = block - 1 # size + 1
247 | Set y = block - 1 \ size + 1
248 | Set @mapGN@("Map", y, x) = $Listbuild(globalNum, fill)
249 | }
250 |
251 | Quit $$$OK
252 | }
253 |
254 | ClassMethod justify(val, len = 4, back = 1) As %String
255 | {
256 | Set result = ""
257 | For i=1:1:len {
258 | Set:'back result = $Char(val#256) _ result
259 | Set:back result = result _ $Char(val#256)
260 | Set val = val\256
261 | }
262 | Quit result
263 | }
264 |
265 | }
266 |
--------------------------------------------------------------------------------
/server/src/Blocks/Router.cls:
--------------------------------------------------------------------------------
1 | Include %syDatabase
2 |
3 | Class Blocks.Router Extends %CSP.REST
4 | {
5 |
6 | XData UrlMap
7 | {
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | }
17 |
18 | ClassMethod WebSocket() As %Status
19 | {
20 | Do ##class(%Library.Device).ReDirectIO(0)
21 | Quit ##class(Blocks.WebSocket).Page(0)
22 | }
23 |
24 | ClassMethod outputJSON(data)
25 | {
26 | #dim %response As %CSP.Response
27 | set %response.ContentType = "application/json"
28 |
29 | if $data(data),$isobject(data) {
30 | if data.%IsA("%ZEN.proxyObject") {
31 | quit data.%ToJSON()
32 | } else {
33 | write data.%ToJSON()
34 | quit $$$OK
35 | }
36 | }
37 |
38 | quit $$$OK
39 | }
40 |
41 | ClassMethod GetStatic(pName As %String = "") As %Status
42 | {
43 | #dim %response As %CSP.Response
44 |
45 | set tName = pName
46 | if tName = "" set tName = "index.html"
47 | set tName = $tr(tName,".","_")
48 | set ext = $zcvt($p(tName,"_",*),"l")
49 | if ##class(%CSP.StreamServer).FileClassify(ext,.type) {
50 | set %response.ContentType = type
51 | }
52 | set xdata = ##class(%Dictionary.XDataDefinition).%OpenId(..%ClassName(1)_"||"_tName)
53 | if $isobject(xdata) {
54 | set status=##class(%XML.TextReader).ParseStream(xdata.Data, .textreader)
55 | set isBase64=xdata.Description["*base64*"
56 | if $isobject(textreader) {
57 | set data=""
58 | while textreader.Read() {
59 | if (textreader.NodeType="chars") {
60 | if isBase64 {
61 | set data=data_textreader.Value
62 | set data=$translate(data,$char(13,10))
63 | set data4Decode=$extract(data,1,$length(data)\4*4)
64 | write $system.Encryption.Base64Decode(data4Decode)
65 | set data=$extract(data,$length(data4Decode)+1,*)
66 | } else {
67 | write textreader.Value
68 | }
69 | }
70 | }
71 | write $system.Encryption.Base64Decode(data)
72 | quit $$$OK
73 | } else {
74 | quit xdata.Data.OutputToDevice()
75 | }
76 | }
77 |
78 | set tName = %request.GetCgiEnv("SCRIPT_FILENAME")
79 | if pName = "" set tName = ##class(%File).NormalizeFilename("index.html", tName)
80 | if ##class(%File).Exists(tName) {
81 | set stream = ##class(%Stream.FileBinary).%New()
82 | set stream.Filename = tName
83 | quit stream.OutputToDevice()
84 | }
85 | quit $$$OK
86 | }
87 |
88 | ClassMethod Ping() As %Status
89 | {
90 | set result = {}
91 | set result.ping = "pong"
92 | set result.session = %session.SessionId
93 | quit ..outputJSON(result)
94 | }
95 |
96 | ClassMethod DBList() As %Status
97 | {
98 | new $namespace
99 | znspace "%SYS"
100 |
101 | write "["
102 | set first = 1
103 |
104 | set tStatement=##class(%SQL.Statement).%New()
105 | set tSC=tStatement.%PrepareClassQuery("Config.Databases","List")
106 | #dim rset As %SQL.StatementResult = tStatement.%Execute()
107 | while rset.%Next() {
108 | set dir=rset.%Get("Directory")
109 | set name=rset.%Get("Name")
110 | set was($zconvert(dir,"L"))=name
111 | do add(name,dir)
112 | }
113 |
114 | set tSC=tStatement.%PrepareClassQuery("SYS.Database","List")
115 | set rset = tStatement.%Execute()
116 | while rset.%Next() {
117 | set dir=rset.%Get("Directory")
118 | continue:$data(was($zconvert(dir,"L")))
119 | do add(dir,dir)
120 | }
121 | write "]"
122 | quit ..outputJSON()
123 | add(name,dir)
124 | set db=##class(SYS.Database).%OpenId(dir)
125 | set blocks=db.Size*1024*1024/db.BlockSize
126 | Set tJSON = ##class(%ZEN.proxyObject).%New()
127 | set tJSON.name = name
128 | set tJSON.directory = dir
129 | set tJSON.blocks = blocks
130 | if 'first write ", "
131 | set first = 0
132 | do tJSON.%ToJSON()
133 | }
134 |
135 | ClassMethod BlockInfo(pBlock) [ ProcedureBlock = 0 ]
136 | {
137 | new $namespace
138 | znspace "%SYS"
139 |
140 | set Directory=$select($isobject($get(%request)):%request.Get("directory"),1:"")
141 | set:Directory="" Directory=$zutil(12,"samples")
142 |
143 |
144 | Set tJSON = ##class(%ZEN.proxyObject).%New()
145 | set tJSON.blockId = pBlock
146 | Set tJSON.nodes = ##class(%ListOfDataTypes).%New()
147 |
148 | #dim error As %Exception.AbstractException = ""
149 | try {
150 | set rc=$$FindPointerBlock^DMREPAIR(Directory,pBlock,.upblock)
151 |
152 | set rc=$$ParseRepairBlock^DMREPAIR(Directory,pBlock,,.OFF,.REPAIR,.REPPRINT,.REPVAL,.REPCCC,.REPLEN,.REPPAD,.REPSUB,.REPBIG,.REPINFO,.N,.TYPE,.LINK,.BIGCOUNT,.PNTLEN,.NEXTPNTLEN,.NEXTPNTVAL,.NEXTPNTOFF,.PNTREF,.NEXTPNTREF,.BLINCVER,.COLLATE,.GARTREE)
153 |
154 | set:$data(OFF) tJSON.offset=OFF
155 | if $data(TYPE) {
156 | set tJSON.type=TYPE
157 | set tJSON.typename=$$GetTypeName^DMREPAIR(TYPE)
158 | }
159 | set:$data(LINK) tJSON.link=LINK
160 | set:$data(BIGCOUNT) tJSON.bigCount=BIGCOUNT
161 |
162 | set:$data(PNTLEN) tJSON.pointerLength=PNTLEN
163 | set:$data(PNTREF("internal"),val) tJSON.pointerRefInt=$system.Encryption.Base64Encode(val)
164 | set:$data(PNTREF("printable"),val) tJSON.pointerRef=val
165 |
166 | set:$data(NEXTPNTLEN) tJSON.nextPointerLength=NEXTPNTLEN
167 | set:$data(NEXTPNTOFF) tJSON.nextPointerOffset=NEXTPNTOFF
168 | set:$data(NEXTPNTREF("internal"),val) tJSON.nextPointerRefInt=$system.Encryption.Base64Encode(val)
169 | set:$data(NEXTPNTREF("printable"),val) tJSON.nextPointerRef=val
170 |
171 | set:$data(COLLATE) tJSON.collate=COLLATE
172 |
173 | for i=1:1:N {
174 |
175 | set tNode=##class(%ArrayOfDataTypes).%New()
176 | do tNode.SetAt(i,"nodeId")
177 |
178 | do:$data(REPAIR(i),repint) tNode.SetAt($system.Encryption.Base64Encode(repint),"internal")
179 | for var="print","ccc","len","pad","sub","big","info" {
180 | set val=@("REP"_$zconvert(var,"U"))@(i)
181 | do tNode.SetAt(val,var)
182 | }
183 | if TYPE'=8,$data(REPVAL) {
184 | set val=$listfromstring(@("REPVAL")@(i))
185 | if TYPE=9 {
186 | do tNode.SetAt($listget(val,1),"blockId")
187 | do tNode.SetAt($listget(val,3),"collate")
188 | } else {
189 | do tNode.SetAt($listget(val,2),"blockId")
190 | }
191 | }
192 |
193 | do tJSON.nodes.Insert(tNode)
194 | }
195 | } catch error {
196 | }
197 |
198 | if $isobject(error) {
199 | quit error.AsStatus()
200 | }
201 |
202 | quit ..outputJSON(tJSON)
203 | }
204 |
205 | }
206 |
--------------------------------------------------------------------------------
/server/src/Blocks/StandaloneInstaller.cls:
--------------------------------------------------------------------------------
1 | Class Blocks.StandaloneInstaller Extends %Projection.AbstractProjection
2 | {
3 |
4 | Projection Reference As Blocks.StandaloneInstaller;
5 |
6 | Parameter CSPAPP As %String = "/blocks";
7 |
8 | Parameter CSPAPPDIR As %String = {##class(%File).SubDirectoryName($system.Util.InstallDirectory(), "csp"_$zcvt(..#CSPAPP,"l"), 1)};
9 |
10 | Parameter CSPAPPDESC As %String = "A WEB application for Cache Blocks Explorer.";
11 |
12 | Parameter ROUTER As %String = "Blocks.Router";
13 |
14 | Parameter NAMESPACE As %String = "BLOCKS";
15 |
16 | Parameter DBNAME As %String = "BLOCKS";
17 |
18 | Parameter DBPATH As %String = {$zu(12,$zcvt(..#DBNAME,"l"))};
19 |
20 | Parameter AUTOINSTALL As %Boolean = 0;
21 |
22 | /// This method is invoked when a class is compiled.
23 | ClassMethod CreateProjection(classname As %String, ByRef parameters As %String, modified As %String, qstruct) As %Status
24 | {
25 | quit:'..#AUTOINSTALL $$$OK
26 | set xdata=##class(%Dictionary.XDataDefinition).%OpenId(..%ClassName(1)_"||Data",0)
27 | quit:'$isobject(xdata) $$$OK
28 |
29 | set logFile=##class(%File).TempFilename("setupBlocksExplorer")
30 |
31 | job ..setup():(:::logFile):0
32 | if '$test {
33 | quit $$$OK
34 | }
35 | set child=$zchild
36 | do { hang 0.1 } while $data(^$JOB(child))
37 |
38 | set fs=##class(%Stream.FileCharacter).%New()
39 | set fs.Filename=logFile
40 | while 'fs.AtEnd {
41 | write !,fs.ReadLine()
42 | }
43 | if ##class(%File).Delete(logFile)
44 | quit $$$OK
45 | }
46 |
47 | ClassMethod setup(ByRef pVars, pLogLevel As %Integer = 3, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator, Internal ]
48 | {
49 | do %code.WriteLine($c(9)_"if '$data(pVars(""FORCE"")) set pVars(""FORCE"")=$g(%qstruct(""force""))")
50 | do %code.WriteLine($c(9)_"set pVars(""CURRENTCLASS"")="""_%classname_"""")
51 | do %code.WriteLine($c(9)_"set pVars(""CURRENTNS"")=$namespace")
52 | do %code.WriteLine($c(9)_"if '$data(pVars(""NAMESPACE"")) set pVars(""NAMESPACE"")=..#NAMESPACE")
53 | do %code.WriteLine($c(9)_"if '$data(pVars(""DBNAME"")) set pVars(""DBNAME"")=..#DBNAME")
54 | do %code.WriteLine($c(9)_"if '$data(pVars(""DBPATH"")) set pVars(""DBPATH"")=..#DBPATH")
55 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPP"")) set pVars(""CSPAPP"")=..#CSPAPP")
56 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPPDIR"")) set pVars(""CSPAPPDIR"")=..#CSPAPPDIR")
57 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPPDESC"")) set pVars(""CSPAPPDESC"")=..#CSPAPPDESC")
58 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPPROUTER"")) set pVars(""CSPAPPROUTER"")=..#ROUTER")
59 | quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "setup")
60 | }
61 |
62 | XData setup [ XMLNamespace = INSTALLER ]
63 | {
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | }
82 |
83 | ClassMethod PrepareData() As %String
84 | {
85 | set tmpFile=##class(%File).TempFilename("xml")
86 | set tmpFileFS=##class(%Stream.FileBinary).%New()
87 | set tmpFileFS.Filename=tmpFile
88 | set xdata=##class(%Dictionary.XDataDefinition).%OpenId(..%ClassName(1)_"||Data",0)
89 | set status=##class(%XML.TextReader).ParseStream(xdata.Data, .textreader)
90 | set data=""
91 | while textreader.Read() {
92 | if (textreader.NodeType="chars") {
93 | set data=data_textreader.Value
94 | set data=$translate(data,$char(13,10))
95 | set data4Decode=$extract(data,1,$length(data)\4*4)
96 | do tmpFileFS.Write($system.Encryption.Base64Decode(data4Decode))
97 | set data=$extract(data,$length(data4Decode)+1,*)
98 | }
99 | }
100 | do tmpFileFS.Write($system.Encryption.Base64Decode(data))
101 | do tmpFileFS.%Save()
102 | quit tmpFile
103 | }
104 |
105 | ClassMethod SetDispatchClass(pCSPName As %String = "", pDispatchClass As %String = "") As %Status
106 | {
107 | new $namespace
108 | znspace "%SYS"
109 | set props("DispatchClass")=pDispatchClass
110 | set props("Recurse")=1
111 | d ##class(Security.Applications).Modify(pCSPName,.props)
112 | quit $$$OK
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/server/src/Blocks/WebSocket.cls:
--------------------------------------------------------------------------------
1 | Class Blocks.WebSocket Extends %CSP.WebSocket
2 | {
3 |
4 | Parameter UseSession = 0;
5 |
6 | Property ResourceName As %String;
7 |
8 | Property ChildPID As %Integer;
9 |
10 | Property Event As %String;
11 |
12 | Method ResourceNameGet() As %String
13 | {
14 | set resName = "blocks" _ $job
15 | if '$system.Event.Defined(resName) {
16 | do $system.Event.Create(resName)
17 | }
18 | quit resName
19 | }
20 |
21 | Method OnPreServer() As %Status
22 | {
23 | Quit $$$OK
24 | }
25 |
26 | Method Server() As %Status
27 | {
28 | Set timeout=.1
29 | set quit=0
30 | #dim exception As %Exception.AbstractException
31 | Set len=32656
32 | while 'quit {
33 | try {
34 | Set data=..Read(.len, .status, timeout)
35 | If $$$ISERR(status),$$$GETERRORCODE(status) = $$$CSPWebSocketClosed {
36 | set quit=1
37 | quit
38 | }
39 | set timeouted = 0
40 | If $$$ISERR(status),$$$GETERRORCODE(status) = $$$CSPWebSocketTimeout {
41 | set timeouted = 1
42 | }
43 | if timeouted {
44 | set sc = ..SendBlocksData()
45 | } else {
46 | set sc = ..Action(data)
47 | }
48 | $$$ThrowOnError(sc)
49 | } catch exception {
50 | do ..Write("error", exception.DisplayString())
51 | set quit=1
52 | }
53 | }
54 | Set status=..EndServer()
55 | Quit $$$OK
56 | }
57 |
58 | Method SendBlocksData() As %Status
59 | {
60 | set st = $$$OK
61 |
62 | quit:..Event="" $$$OK
63 |
64 | set child = ..ChildPID
65 | set resName = ..ResourceName
66 |
67 | set responseData = ##class(%DynamicArray).%New()
68 | kill blocksData
69 | set countData=0
70 | set atEnd=0
71 | while 'atEnd {
72 | set $lb(sc,data)=$system.Event.WaitMsg(resName, 0)
73 | if sc<=0,'$data(^$JOB(child)) {
74 | set atEnd=1
75 | quit
76 | }
77 | continue:data=""
78 | set $listbuild(parent,block,global,fill)=data
79 | if $p(..Event,"_",2)="tree" {
80 | if '$data(blocksData(parent)) {
81 | set blocksData(parent)=##class(%DynamicObject).%New()
82 | set blocksData(parent).block = parent
83 | set blocksData(parent).child = ##class(%DynamicArray).%New()
84 | do responseData.%Push(blocksData(parent))
85 | }
86 | do blocksData(parent).child.%Push(block)
87 | } else {
88 | if '$data(blocksData(global)) {
89 | set blocksData(global)=##class(%DynamicObject).%New()
90 | set blocksData(global).global = global
91 | set blocksData(global).blocks = ##class(%DynamicArray).%New()
92 | do responseData.%Push(blocksData(global))
93 | }
94 | set blockInfo = ##class(%DynamicArray).%New()
95 | do blockInfo.%Push(block)
96 | do blockInfo.%Push(fill)
97 | do blocksData(global).blocks.%Push(blockInfo)
98 | kill blockInfo
99 | }
100 | quit:$i(countData)>=1000
101 | }
102 | if responseData.Size > 0 {
103 | do ..Write(..Event, responseData)
104 | }
105 | if atEnd {
106 | set ..Event = ""
107 | }
108 |
109 | quit $$$OK
110 | }
111 |
112 | Method Action(obj As %DynamicObject) As %Status
113 | {
114 | set st = $$$OK
115 | #dim exception As %Exception.AbstractException
116 | try {
117 | quit:obj=""
118 | set action = obj.event
119 | set data = ##class(%DynamicArray).%New()
120 | if action="ping" {
121 | set sc=..Write(action, "pong")
122 | } elseif $p(action,"_")="blocks" {
123 | set asTree=($piece(action,"_",2)="tree")
124 |
125 | do $system.Event.Clear(..ResourceName)
126 |
127 | job ##class(Blocks.WebSocket).GetBlocks(obj.data.directory, asTree, ..ResourceName)
128 | set child=$zchild
129 | set ..ChildPID = child
130 |
131 | set ..Event = action
132 | }
133 |
134 | } catch exception {
135 | set st = exception.AsStatus()
136 | }
137 | quit st
138 | }
139 |
140 | Method OnPostServer() As %Status
141 | {
142 | Quit $$$OK
143 | }
144 |
145 | ClassMethod GetBlocks(aDirectory As %String = "", aAsTree As %Boolean = 0, ResourceName As %String = "")
146 | {
147 | set resName="blocks"_$zparent
148 | quit:aDirectory="" 0
149 |
150 | new $namespace
151 | znspace "%sys"
152 |
153 | OPEN 63:"^^"_aDirectory
154 |
155 | try {
156 | set tSC=..ReadBlocks(aAsTree, 3, "", "", 0, ResourceName)
157 | } catch ex {
158 | set tSC=ex.AsSystemError()
159 | }
160 |
161 | CLOSE 63
162 | $$$ThrowOnError(tSC)
163 | quit $$$OK
164 | }
165 |
166 | ClassMethod ReadBlocks(aAsTree As %Boolean = 0, aBlockId As %Integer = 3, aParentBlock = "", aGlobal As %String = "", aHasLong = 0, ResourceName As %String = "")
167 | {
168 | #define toInt(%bytes) ($a(%bytes,1)+($a(%bytes,2)*256)+($a(%bytes,3)*65536)+($a(%bytes,4)*16777216))
169 |
170 | new $namespace
171 | znspace "%SYS"
172 | quit:aBlockId=0 0
173 |
174 | set blockSize=8192
175 |
176 | #dim error As %Exception.AbstractException = ""
177 | try {
178 | View aBlockId
179 | if aParentBlock'="" {
180 | set offset=$view(0,0,-4)
181 | set offset=$$$toInt(offset)+28
182 | do add(aParentBlock,aBlockId,aGlobal,offset)
183 | }
184 | set blockType=$view($Zutil(40,32,1),0,1)
185 | set nodes=0
186 | if blockType=8 {
187 | if aHasLong {
188 | For N=1:1 {
189 | Set X=$VIEW(N*2,-6)
190 | Quit:X=""
191 | set gdview=$ascii(X)
192 | if $listfind($listbuild(5,7,3),gdview) {
193 | set cnt=$piece(X,",",2)
194 | set blocks=$piece(X,",",4,*)
195 | for i=1:1:cnt {
196 | set nextBlock=$piece(X,",",3+i)
197 | do add(aBlockId,nextBlock,aGlobal,blockSize)
198 | }
199 | }
200 | }
201 | }
202 | } else {
203 | For N=1:1 {
204 | Set X=$VIEW(N-1*2+1,-6)
205 | Quit:X=""
206 | Set nextBlock=$VIEW(N*2,-5)
207 | if blockType=9 set aGlobal=X
208 | set haslong=0
209 | if $piece($view(N*2,-6),",",1) {
210 | set haslong=1
211 | }
212 | set nodes($increment(nodes))=$listbuild(nextBlock,aGlobal,haslong)
213 | }
214 | }
215 |
216 | for i=1:1:nodes {
217 | do ..ReadBlocks(aAsTree, $listget(nodes(i)), aBlockId, $listget(nodes(i),2), $listget(nodes(i),3), ResourceName)
218 | }
219 | } catch error {
220 |
221 | }
222 | #; finally
223 |
224 | if $isobject(error) Throw error
225 |
226 | quit $$$OK
227 | add(parentBlock,blockId,global,offset)
228 | set data=$listbuild(parentBlock,blockId)
229 | if 'aAsTree set data=data_$listbuild(global,$j(offset/blockSize*100,0,0))
230 | do $system.Event.Signal(ResourceName, data)
231 | }
232 |
233 | /// Reads up to len characters from the client.
234 | /// If the call is successful the status (sc) will be returned as $$$OK, otherwise an error code of $$$CSPWebSocketTimeout
235 | /// indicates a timeout and $$$CSPWebSocketClosed indicates that the client has terminated the WebSocket.
236 | Method Read(ByRef len As %Integer = 32656, ByRef sc As %Status, timeout As %Integer = 86400) As %String
237 | {
238 | Set json = ##super(len, .sc, timeout)
239 | Do:$$$ISERR(sc) ..EndServer()
240 | quit:json="" ""
241 | try {
242 | set obj = ##class(%DynamicObject).%FromJSON(json)
243 | } catch ex {
244 | set sc = ex.AsStatus()
245 | set obj = ""
246 | }
247 | Quit obj
248 | }
249 |
250 | Method Write(eventName As %String = "", data As %String) As %Status
251 | {
252 | if '$d(data) {
253 | quit ##super(eventName)
254 | }
255 | set response = ##class(%DynamicObject).%New()
256 | set response.event = eventName
257 | set response.data = data
258 |
259 | set json = response.%ToJSON()
260 | quit ##super(json)
261 | }
262 |
263 | }
264 |
--------------------------------------------------------------------------------
/server/src/DevInstaller.cls:
--------------------------------------------------------------------------------
1 | Class Blocks.DevInstaller
2 | {
3 |
4 | XData setup [ XMLNamespace = INSTALLER ]
5 | {
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | }
42 |
43 | ClassMethod setupWithVars(pRootDir As %String = "")
44 | {
45 | set vars("FOLDER") = ##class(%File).NormalizeDirectory(pRootDir)
46 | quit ..setup(.vars)
47 | }
48 |
49 | ClassMethod setup(ByRef pVars, pLogLevel As %Integer = 3, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator ]
50 | {
51 | do %code.WriteLine($c(9)_"set pVars(""CURRENTCLASS"")="""_%classname_"""")
52 | do %code.WriteLine($c(9)_"set pVars(""CURRENTNS"")=$namespace")
53 | quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "setup")
54 | }
55 |
56 | ClassMethod SetDispatchClass(pCSPName As %String = "", pDispatchClass As %String = "") As %Status
57 | {
58 | new $namespace
59 | znspace "%SYS"
60 | set props("DispatchClass")=pDispatchClass
61 | set props("Recurse")=1
62 | set props("AutheEnabled") = 64
63 | set props("MatchRoles") = ":%ALL"
64 | d ##class(Security.Applications).Modify(pCSPName,.props)
65 | quit $$$OK
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/web/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "targets": {
7 | "browsers": [
8 | "last 2 versions",
9 | "safari >= 7"
10 | ]
11 | }
12 | }
13 | ]
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true
5 | },
6 | "extends": "eslint:recommended",
7 | "rules": {
8 | "indent": [
9 | "error",
10 | 2
11 | ],
12 | "linebreak-style": [
13 | "error",
14 | "unix"
15 | ],
16 | "quotes": [
17 | "error",
18 | "single"
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/web/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12-slim
2 |
3 | # RUN apk update && apk add git
4 |
5 | WORKDIR /opt/app
6 |
7 | COPY package.json package-lock.json ./
8 |
9 | RUN npm install
10 |
11 | ENV PATH="$PATH:./node_modules/.bin"
12 |
13 | VOLUME /opt/app/node_modules
14 | VOLUME /opt/app/build
15 |
16 | CMD ["npm", "run", "serve:docker"]
17 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Cache Blocks explorer
7 |
8 |
9 |
10 |
11 |
12 |
28 |
29 |
30 |
31 |
32 |
33 |
40 |
41 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/web/js/blocksViewer.js:
--------------------------------------------------------------------------------
1 | var $ = require('jquery');
2 | var joint = require('jointjs');
3 | require('./joint.shapes.blocks')
4 |
5 | export class BlocksViewer {
6 |
7 | constructor(app, container) {
8 | this.app = app
9 | this.container = $(container)
10 | this.blocks = []
11 | this.blockData = []
12 | this.blocksParent = []
13 | this.blocksLeft = []
14 |
15 | this.ZOOM_DELTA = 0.2
16 | this.PAPER_SCALE = 1
17 | this.MIN_PAPER_SCALE = 0.2
18 | this.MAX_PAPER_SCALE = 2
19 | this.SYMBOL_12_WIDTH = 7
20 |
21 | this.init()
22 | }
23 |
24 | reset() {
25 | this.blocks = []
26 | this.blockData = []
27 | this.blocksParent = []
28 | this.blocksLeft = []
29 |
30 | this.graph.resetCells()
31 | }
32 |
33 | get(blockId) {
34 | var self = this
35 |
36 | if (this.blocks[blockId]) {
37 | var block = this.graph.getCell(this.blocks[blockId])
38 | block.remove()
39 | }
40 | this.app.load('rest/block/' + blockId, {
41 | directory: this.app.database
42 | }, function (data) {
43 | self.add(blockId, data)
44 | })
45 | }
46 |
47 | loadTree(data) {
48 | var self = this
49 |
50 | var addBlock = function (blockId, parentBlock) {
51 | var parent = (!parentBlock) ? null : self.blocks[parentBlock] || addBlock(parentBlock, null)
52 |
53 | var block = new joint.shapes.basic.Circle({
54 | attrs: {
55 | text: {
56 | text: blockId
57 | }
58 | }
59 | })
60 |
61 | self.blocks[blockId] = block
62 | self.graph.addCell(block)
63 |
64 | if (parent) {
65 | var link = new joint.dia.Link({
66 | source: {
67 | id: parent.id
68 | },
69 | target: {
70 | id: block.id
71 | }
72 | })
73 | self.graph.addCell(link)
74 | } else {
75 | block.position(50, 50)
76 | }
77 |
78 | return block
79 | }
80 |
81 | _.each(data, function (parentBlock) {
82 | _.each(parentBlock.child, function (child) {
83 | addBlock(child, parentBlock.block)
84 | })
85 | })
86 | }
87 |
88 | add(blockId, blockData) {
89 | var self = this
90 |
91 | var block = new joint.shapes.blocks.Block({
92 | position: {
93 | x: 0,
94 | y: 0
95 | },
96 | blockData: blockData,
97 | SYMBOL_12_WIDTH: this.SYMBOL_12_WIDTH
98 | })
99 | this.blocks[blockId] = block.id
100 | this.graph.addCell(block)
101 |
102 | var parentBlock = this.blocksParent[blockId]
103 | if (parentBlock) {
104 | var link = new joint.shapes.blocks.Link({
105 | source: {
106 | id: this.blocks[parentBlock],
107 | port: 'node' + blockId
108 | },
109 | target: {
110 | id: block.id,
111 | port: 'up'
112 | }
113 | })
114 | self.graph.addCell(link)
115 | }
116 |
117 | var leftBlock = this.blocksLeft[blockId]
118 | if (leftBlock) {
119 | var link = new joint.shapes.blocks.Link({
120 | source: {
121 | id: this.blocks[leftBlock],
122 | port: 'right'
123 | },
124 | target: {
125 | id: block.id,
126 | port: 'left'
127 | }
128 | })
129 | self.graph.addCell(link)
130 | }
131 |
132 | if (blockData.link && blockData.link > 0) {
133 | self.blocksLeft[blockData.link] = blockId
134 | }
135 | _.each(blockData.nodes, function (node, nodeId) {
136 | var nodeBlockId = node.blockId || 0
137 | if (nodeBlockId === 0) return
138 |
139 | self.blocksParent[node.blockId] = blockId
140 | })
141 | }
142 |
143 | layout(options) {
144 | var rankSep = 60
145 | var nodeSep = 40
146 | var self = this
147 | options = options || {}
148 |
149 | if (!options.parent) {
150 | return
151 | }
152 |
153 | var parentBlock = this.graph.getCell(options.parent)
154 | var outPorts = parentBlock.get('outPorts')
155 | var links = this.graph.getConnectedLinks(parentBlock, {
156 | outbound: true
157 | })
158 | var elements = {},
159 | count = 0,
160 | fullHeight = 0
161 | _.each(links, function (link, i) {
162 | if (link.get('target').port === 'up') {
163 | var child = self.graph.getCell(link.get('target').id)
164 | fullHeight += child.getBBox().height
165 | count += 1
166 | var pos = outPorts.indexOf(link.get('source').port)
167 | elements[pos] = child
168 | }
169 | })
170 | fullHeight += (count - 1) * nodeSep
171 |
172 | var left = parentBlock.getBBox().x + parentBlock.getBBox().width + rankSep
173 | var top = parentBlock.getBBox().y - (fullHeight - parentBlock.getBBox().height) / 2
174 |
175 | _.each(elements, function (block, i) {
176 | block.set('position', {
177 | x: left,
178 | y: top
179 | })
180 | top += block.getBBox().height
181 | top += nodeSep
182 | })
183 | }
184 |
185 | init() {
186 | let self = this
187 | let relP = {
188 | x: 0,
189 | y: 0,
190 | trigger: false
191 | }
192 |
193 |
194 | self.app.ws.bind('blocks_tree', function (data) {
195 | self.loadTree(data)
196 | })
197 |
198 | this.graph = new joint.dia.Graph
199 |
200 | this.paper = new joint.dia.Paper({
201 | el: this.container,
202 | width: this.container.outerWidth(),
203 | height: this.container.outerHeight(),
204 | gridSize: 10,
205 | perpendicularLinks: true,
206 | model: this.graph,
207 | elementView: joint.shapes.blocks.BlockView
208 | })
209 |
210 | this.paper.toSVG = (callback) => {
211 | var svg = document.querySelector('svg');
212 | var data = (new XMLSerializer()).serializeToString(svg)
213 | callback(data)
214 | }
215 |
216 | this.paper.toPNG = (callback) => {
217 | var svg = document.querySelector('svg'),
218 | data = (new XMLSerializer()).serializeToString(svg),
219 | width = this.paper.getContentBBox().width,
220 | height = this.paper.getContentBBox().height
221 |
222 | var canvas = document.createElement('canvas')
223 | var ctx = canvas.getContext('2d')
224 | canvas.setAttribute("width", width);
225 | canvas.setAttribute("height", height);
226 |
227 | var img = new Image()
228 | img.width = width
229 | img.height = height
230 | img.onload = () => {
231 | ctx.drawImage(img, 0, 0, img.width, img.height);
232 |
233 | canvas.toBlob((blob) => {
234 | console.log(blob)
235 | callback(blob)
236 | })
237 | }
238 | img.src = 'data:image/svg+xml,' + data
239 | }
240 |
241 | this.graph.on('add', function (cell) {
242 | if (cell.isLink()) {
243 | self.layout({
244 | parent: cell.get('source').id
245 | })
246 | var block = self.graph.getCell(cell.get('target').id)
247 | var bbox = block.getBBox()
248 | // self.paperScroller.center(bbox.x + (bbox.width / 2), bbox.y)
249 | } else {
250 | self.zoom(self.MAX_PAPER_SCALE)
251 | self.zoomToFit()
252 | }
253 | })
254 |
255 | this.graph.on('remove', function (cell, collection, options) {
256 | if (cell.isLink()) {
257 | var child = this.getCell(cell.get('target').id)
258 | if (child) {
259 | child.remove()
260 | }
261 | } else {
262 | var blockId = cell.get('blockData').blockId
263 | delete self.blocks[blockId]
264 | self.graph.removeCells(cell)
265 | self.layout()
266 | }
267 | })
268 |
269 | this.paper.on("blank:pointerdown", function (e) {
270 | relP.x = e.pageX
271 | relP.y = e.pageY
272 | relP.trigger = true
273 | })
274 |
275 | this.paper.on("blank:pointerup", function (e) {
276 | if (!relP.trigger) return
277 | self.paper.setOrigin(
278 | self.paper.options.origin.x + e.pageX - relP.x,
279 | self.paper.options.origin.y + e.pageY - relP.y
280 | )
281 | relP.trigger = false
282 | })
283 |
284 | var moveHandler = (e) => {
285 | if (!relP.trigger) {
286 | return
287 | }
288 | this.paper.setOrigin(
289 | this.paper.options.origin.x + e.pageX - relP.x,
290 | this.paper.options.origin.y + e.pageY - relP.y
291 | );
292 | relP.x = e.pageX; relP.y = e.pageY;
293 | }
294 |
295 | this.container.on('mousemove', moveHandler)
296 | this.container.on('mousetouch', moveHandler)
297 |
298 | this.paper.on('cell:nodeclick', function (cell, evt, index) {
299 | var node = cell.model.blockData.nodes[index]
300 | if (node.blockId) {
301 | self.get(node.blockId)
302 | }
303 | })
304 |
305 | this.app.elements.zoomInBtn.addEventListener('click', function () {
306 | self.zoom(self.ZOOM_DELTA)
307 | })
308 | this.app.elements.zoomOutBtn.addEventListener('click', function () {
309 | self.zoom(-self.ZOOM_DELTA)
310 | })
311 | this.app.elements.zoomToFitBtn.addEventListener('click', function () {
312 | self.zoomToFit()
313 | // self.zoom(self.MAX_PAPER_SCALE)
314 | })
315 | this.app.elements.downloadSVGBtn.addEventListener('click', () => {
316 |
317 | this.paper.toSVG((svg) => {
318 | svg = new Blob([svg], {
319 | type: 'image/svg+xml'
320 | })
321 | var link = $('')
322 | .hide()
323 | .attr('href', URL.createObjectURL(svg))
324 | .attr('download', 'blocks.svg')
325 | .text('download')
326 | link.get(0).click()
327 | setTimeout(function () {
328 | link.remove()
329 | }, 10)
330 | })
331 | })
332 | this.app.elements.downloadPNGBtn.addEventListener('click', function () {
333 | self.paper.toPNG((png) => {
334 | var link = $('')
335 | .hide()
336 | .attr('href', URL.createObjectURL(png))
337 | .attr('download', 'blocks.png')
338 | .text('download')
339 | link.get(0).click()
340 | setTimeout(function () {
341 | link.remove()
342 | }, 10)
343 | })
344 | })
345 |
346 | this.SYMBOL_12_WIDTH = (function () {
347 | var e = document.createElementNS('http://www.w3.org/2000/svg', 'text'),
348 | s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
349 | w
350 | s.appendChild(e)
351 | s.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
352 | s.setAttribute('version', '1.1')
353 | e.setAttribute('font-family', 'monospace')
354 | e.setAttribute('font-size', '12')
355 | e.textContent = 'aBcDeFgGhH'
356 | document.body.appendChild(s)
357 | w = e.getBBox().width / 10
358 | s.parentNode.removeChild(s)
359 | return w
360 | })()
361 |
362 | this.initWS()
363 | }
364 |
365 | initWS() {
366 | var self = this
367 |
368 | this.app.ws.onmessage = function () {
369 | if (self.app.viewType.is(':checked')) {
370 | return
371 | }
372 | self.wsmessage.apply(self, arguments)
373 | }
374 | }
375 |
376 | wsmessage(event) {
377 | var self = this
378 | try {
379 | var data = JSON.parse(event.data)
380 | $.each(data, function (i, glob) {
381 |
382 | })
383 | } catch (ex) {
384 |
385 | }
386 | }
387 |
388 | zoom(delta) {
389 | var scaleOld = this.PAPER_SCALE
390 | var scaleDelta;
391 |
392 | var sw = this.container.width(),
393 | sh = this.container.height(),
394 | side = delta > 0 ? 1 : -1,
395 | ox = this.paper.options.origin.x,
396 | oy = this.paper.options.origin.y;
397 | if (typeof delta === "number") {
398 | this.PAPER_SCALE += delta * Math.min(
399 | 0.3,
400 | Math.abs(this.PAPER_SCALE - (delta < 0 ? this.MIN_PAPER_SCALE : this.MAX_PAPER_SCALE)) / 2
401 | );
402 | } else { this.PAPER_SCALE = 1; }
403 | this.paper.scale(this.PAPER_SCALE, this.PAPER_SCALE);
404 | scaleDelta = side *
405 | (side > 0 ? this.PAPER_SCALE / scaleOld - 1 : (scaleOld - this.PAPER_SCALE) / scaleOld);
406 | this.paper.setOrigin(
407 | ox - (sw / 2 - ox) * scaleDelta,
408 | oy - (sh / 2 - oy) * scaleDelta
409 | );
410 | }
411 |
412 | zoomToFit() {
413 | const padding = 20
414 | this.paper.scale(1, 1)
415 |
416 | var sw = this.container.width() - (padding * 2),
417 | sh = this.container.height() - (padding * 2),
418 | ox = this.paper.options.origin.x,
419 | oy = this.paper.options.origin.y,
420 | bbox = this.paper.getContentBBox();
421 |
422 | this.PAPER_SCALE = Math.max(
423 | this.MIN_PAPER_SCALE,
424 | Math.min(
425 | this.MAX_PAPER_SCALE,
426 | Math.min(
427 | sw / bbox.width,
428 | sh / bbox.height,
429 | )
430 | )
431 | )
432 |
433 | this.paper.scale(this.PAPER_SCALE, this.PAPER_SCALE)
434 | bbox = this.paper.getContentBBox()
435 | this.paper.setOrigin(
436 | (sw - bbox.width) / 2 + padding,
437 | (sh - bbox.height) / 2 + padding
438 | )
439 |
440 | }
441 | }
442 |
--------------------------------------------------------------------------------
/web/js/joint.shapes.blocks.js:
--------------------------------------------------------------------------------
1 | var $ = require('jquery');
2 | var joint = require('jointjs');
3 | var V = joint.V
4 |
5 | joint.shapes.blocks = {}
6 |
7 | joint.shapes.blocks.Block = joint.shapes.devs.Model.extend(_.extend({}, joint.shapes.basic.PortsModelInterface, {
8 |
9 | markup: [
10 | '',
11 | '',
12 | '',
13 | '',
14 | '',
15 | '',
16 | '',
17 | '',
18 | '',
19 | '',
20 | '',
21 | '',
22 | '',
23 | ''
24 | ].join(''),
25 | portMarkup: [
26 | '',
27 | '',
28 | ''
29 | ].join(''),
30 |
31 | defaults: joint.util.deepSupplement({
32 |
33 | type: 'blocks.Block',
34 | MIN_WIDTH: 100,
35 | size: {
36 | width: 150,
37 | height: 200
38 | },
39 |
40 | extPorts: ['up', 'left', 'right'],
41 | outPorts: [],
42 |
43 | attrs: {
44 | '.': {
45 | magnet: true
46 | },
47 | rect: {
48 | stroke: 'black',
49 | width: 150
50 | },
51 | text: {
52 | 'fill': 'black',
53 | 'font-size': 12,
54 | 'pointer-events': 'none'
55 | },
56 | '.blocks-id-rect': {},
57 | '.blocks-id-text': {
58 | 'ref': '.blocks-id-rect',
59 | 'ref-x': 5,
60 | 'ref-y': 3
61 | },
62 | '.blocks-info-rect': {},
63 | '.blocks-info-text': {
64 | 'ref': '.blocks-info-rect',
65 | 'ref-x': 5,
66 | 'ref-y': 3
67 | },
68 | '.blocks-nodes-label': {
69 | ref: '.blocks-nodes-rect',
70 | 'font-size': 10
71 | },
72 | '.blocks-nodes-rect': {},
73 | '.blocks-nodes-text': {
74 | 'ref': '.blocks-nodes-rect',
75 | 'ref-y': 2,
76 | 'ref-x': 8
77 | },
78 | '.port-body': {
79 | r: 4,
80 | magnet: true,
81 | stroke: '#000000'
82 | }
83 | },
84 |
85 | dataBlock: {}
86 |
87 | }, joint.shapes.basic.Generic.prototype.defaults),
88 |
89 | initialize: function () {
90 | var self = this
91 | var SYMBOL_12_WIDTH = this.get('SYMBOL_12_WIDTH') || 6.6
92 | this.blockData = this.get('blockData')
93 |
94 | this.rects = [{
95 | type: 'id',
96 | text: [
97 | 'Block # ' + this.blockData.blockId
98 | ]
99 | }, {
100 | type: 'info',
101 | text: [
102 | 'Type: ' + this.blockData.type + ' ' +
103 | this.blockData.typename,
104 | 'Link: ' + (this.blockData.link || 0),
105 | 'Offset: ' + (this.blockData.offset || 0)
106 | ]
107 | }]
108 |
109 | var nodesRect = {
110 | type: 'nodes',
111 | text: []
112 | }
113 | var outPorts = []
114 | _.each(this.blockData.nodes, function (node, i) {
115 | var text = (nodesRect.text.length + 1) + ') ' + node.print
116 | if (node.blockId) {
117 | text += ' - ' + node.blockId
118 | outPorts.push('node' + node.blockId)
119 | }
120 | nodesRect.text.push(text)
121 | })
122 | nodesRect.text.push('')
123 |
124 | this.get('attrs').blocktype = this.blockData.type
125 |
126 | this.rects.push(nodesRect)
127 | this.set('outPorts', outPorts)
128 |
129 | this.defaults.size.width = Math.max(this.defaults.MIN_WIDTH, 150)
130 | _.each(this.rects, function (rect) {
131 | rect.text.forEach(function (s) {
132 | var t = s.length * SYMBOL_12_WIDTH + 20
133 | if (t > self.defaults.size.width) {
134 | self.defaults.size.width = t
135 | }
136 | })
137 | })
138 |
139 | switch (this.blockData.type) {
140 | case 9:
141 | this.fillColor = 'coral'
142 | break
143 | case 70:
144 | this.fillColor = 'moccasin'
145 | break
146 | case 8:
147 | this.fillColor = 'palegreen'
148 | break
149 | case 6:
150 | this.fillColor = 'honeydew'
151 | break
152 | case 24:
153 | case 66:
154 | default:
155 | this.fillColor = 'powderblue'
156 | }
157 |
158 | this.updateRectangles()
159 |
160 | this.updatePortsAttrs()
161 | this.on('change:allPorts', this.updatePortsAttrs, this)
162 |
163 | this.on('change:name change:attributes change:methods', function () {
164 | this.updateRectangles()
165 | this.trigger('blocks-update')
166 | }, this)
167 |
168 | joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments)
169 | },
170 |
171 | updateRectangles: function () {
172 | var self = this
173 | var attrs = this.get('attrs')
174 |
175 | var offsetY = 0
176 |
177 | _.each(this.rects, function (rect) {
178 | var lines = _.isArray(rect.text) ? rect.text : [{
179 | text: rect.text
180 | }]
181 |
182 | var rectHeight = (lines.length - (rect.type === 'nodes' ? 1 : 0)) * 12 + (lines.length ? 4 : 0)
183 | var rectText = attrs['.blocks-' + rect.type + '-text']
184 | var rectRect = attrs['.blocks-' + rect.type + '-rect']
185 |
186 | if (rect.type === 'id' || rect.type === 'info') {
187 | rectRect.fill = self.fillColor
188 | }
189 |
190 | rectText.text = lines.join('\n')
191 | rectRect.transform = 'translate(0,' + offsetY + ')'
192 |
193 | rectRect.height = rectHeight
194 | offsetY += rectHeight
195 | })
196 |
197 | this.attributes.size.height = offsetY
198 | this.attributes.size.width = this.defaults.size.width
199 | this.attributes.attrs.rect.width = this.defaults.size.width
200 | },
201 | getPortAttrs: function (portName, index, total, selector, type) {
202 | var attrs = {}
203 |
204 | var portClass = 'port' + index
205 | var portSelector = selector + '>.' + portClass
206 | var portBodySelector = portSelector + '>.port-body'
207 |
208 | attrs[portBodySelector] = {
209 | port: {
210 | id: portName || _.uniqueId(type),
211 | type: type
212 | }
213 | }
214 |
215 | var ref,
216 | refX,
217 | refY,
218 | refDX,
219 | refDY
220 | if (portName === 'up') {
221 | ref = '.blocks-id-rect'
222 | refY = 8
223 | } else if (portName === 'left') {
224 | ref = '.blocks-id-rect'
225 | refX = 0.5
226 | refY = 0
227 | } else if (portName === 'right') {
228 | ref = '.block'
229 | refX = 0.5
230 | refDY = 0
231 | } else {
232 | ref = '.blocks-nodes-rect'
233 | refY = (index + 0.5) * (1 / total)
234 | refDX = 0
235 | }
236 |
237 | attrs[portSelector] = {
238 | ref: ref,
239 | 'ref-x': refX,
240 | 'ref-y': refY,
241 | 'ref-dx': refDX,
242 | 'ref-dy': refDY
243 | }
244 |
245 | return attrs
246 | },
247 | updatePortsAttrs: function () {
248 | var self = this;
249 | var currAttrs = this.get('attrs')
250 | this._portSelectors = this._portSelectors || [];
251 | this._portSelectors.forEach((selector) => {
252 | if (currAttrs[selector]) delete currAttrs[selector]
253 | })
254 |
255 | this._portSelectors = []
256 |
257 | var attrs = {}
258 |
259 | this.get('extPorts').forEach((portName, index, ports) => {
260 | var portAttributes = self.getPortAttrs(portName, index, 1, '.' + portName + 'Port', 'ext')
261 | self._portSelectors = self._portSelectors.concat(_.keys(portAttributes))
262 | _.extend(attrs, portAttributes)
263 | })
264 |
265 | this.get('outPorts').forEach((portName, index, ports) => {
266 | var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out')
267 | this._portSelectors = this._portSelectors.concat(_.keys(portAttributes))
268 | _.extend(attrs, portAttributes)
269 | })
270 |
271 | this.attr(attrs, {
272 | silent: true
273 | })
274 | this.processPorts()
275 | this.trigger('process:ports')
276 | }
277 | }))
278 |
279 | joint.shapes.blocks.Link = joint.dia.Link.extend({
280 |
281 | // defaults: {
282 | // type: 'blocks.Link',
283 | // smooth: true,
284 | // attrs: {
285 | // '.connection': {
286 | // 'stroke-width': 1
287 | // },
288 | // '.marker-target': {
289 | // d: 'M7,0L0,4L7,7L5,4z',
290 | // fill: 'black'
291 | // }
292 | // },
293 | // router: {
294 | // name: 'manhattan'
295 | // },
296 | // connector: {
297 | // name: 'rounded'
298 | // }
299 | // }
300 | })
301 |
302 | joint.shapes.blocks.BlockView = joint.dia.ElementView.extend(_.extend({}, joint.shapes.basic.PortsViewInterface, {
303 | renderPorts: function () {
304 |
305 | var $extPorts = this.$('.extPorts').empty()
306 | var $outPorts = this.$('.outPorts').empty()
307 |
308 | var portTemplate = _.template(this.model.portMarkup)
309 |
310 | _.each(_.filter(this.model.ports, function (p) {
311 | return p.type === 'ext'
312 | }), function (port, index) {
313 |
314 | var $port = $extPorts.filter('.' + port.id + 'Port')
315 | $port.append(V(portTemplate({
316 | id: index,
317 | port: port
318 | })).node)
319 | })
320 |
321 | _.each(_.filter(this.model.ports, function (p) {
322 | return p.type === 'out'
323 | }), function (port, index) {
324 |
325 | $outPorts.append(V(portTemplate({
326 | id: index,
327 | port: port
328 | })).node)
329 | })
330 | },
331 |
332 | pointerclick: function (evt, x, y) {
333 | if ($(evt.target).parent().is('.blocks-nodes-text')) {
334 | var index = $(evt.target).index()
335 | this.notify('cell:nodeclick', evt, index)
336 | }
337 | }
338 | }))
339 |
340 | if (typeof exports === 'object') {
341 | module.exports = joint.shapes.blocks
342 | }
343 |
344 |
--------------------------------------------------------------------------------
/web/js/main.js:
--------------------------------------------------------------------------------
1 | var $ = require('jquery');
2 | import {BlocksViewer} from './blocksViewer';
3 | import {FancyWebSocket} from './wsEventDispatcher';
4 | var MapViewer = require('./mapViewer');
5 | require('../styles/main.scss')
6 |
7 | var app
8 | $(function () {
9 | app = new App()
10 | window.app = app;
11 |
12 | })
13 |
14 | var App = function () {
15 | this.databaseSelect = $('#databaseSelect')
16 | this.viewType = $('#viewType')
17 | this.database = null
18 | this.blockInfo = $('#header .blockInfo')
19 |
20 | this.elements = {
21 | viewType: $('#viewType').get(0),
22 | blocksViewer: $('#blocksViewer').get(0),
23 | zoomInBtn: $('#btnZoomIn').get(0),
24 | zoomOutBtn: $('#btnZoomOut').get(0),
25 | zoomToFitBtn: $('#btnZoomToFit').get(0),
26 | downloadSVGBtn: $('#btnDownloadSVG').get(0),
27 | downloadPNGBtn: $('#btnDownloadPNG').get(0),
28 | mapViewer: $('#map canvas').get(0)
29 | }
30 |
31 | var wsUrl = ((window.location.protocol == "https:") ? "wss:" : "ws:" + "//" + window.location.host)
32 | wsUrl += window.location.pathname + 'websocket'
33 | this.ws = new FancyWebSocket(wsUrl)
34 |
35 | this.ws.bind('error', function (data) {
36 | console.log(data)
37 | })
38 |
39 | this.blocksViewer = new BlocksViewer(this, this.elements.blocksViewer)
40 |
41 | this.mapViewer = new MapViewer(this, this.elements.mapViewer)
42 |
43 | this.init()
44 |
45 | return this
46 | }
47 |
48 | App.prototype.checkWSState = function () {
49 | var self = this
50 |
51 |
52 | setTimeout(function () {
53 | self.checkWSState()
54 | }, 1000)
55 | }
56 |
57 | App.prototype.load = function (url, data, callback) {
58 | var self = this
59 |
60 | $.ajax({
61 | url: url,
62 | type: data ? 'POST' : 'GET',
63 | data: data,
64 | dataType: 'json',
65 | complete: function () {
66 |
67 | },
68 | success: function (data, status) {
69 | callback.apply(self, [data, status])
70 | }
71 | })
72 | }
73 |
74 | App.prototype.updateDatabases = function (data) {
75 | var self = this
76 |
77 | this.databaseSelect.find('option').remove()
78 |
79 | $('