├── .babelrc ├── .codebeatignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── node.js.yml ├── .gitignore ├── docker ├── README.md ├── build-and-upload.sh ├── conf │ └── master ├── docker-compose.yml ├── dockerfiles │ ├── conf │ │ ├── minion │ │ ├── nginx │ │ │ ├── default.conf │ │ │ └── nginx.conf │ │ └── supervisord.conf │ ├── dockerfile-saltgui-nginx │ ├── dockerfile-saltmaster │ ├── dockerfile-saltminion-centos │ ├── dockerfile-saltminion-debian │ └── dockerfile-saltminion-ubuntu └── srv │ ├── pillar │ ├── secrets.sls │ └── top.sls │ └── salt │ ├── top.sls │ └── vim │ ├── init.sls │ └── map.jinja ├── docs ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── PERMISSIONS.md ├── README.md ├── _config.yml ├── assets │ ├── favicon │ │ └── favicon.ico │ ├── fonts │ │ └── Noto-Sans-regular │ │ │ └── Noto-Sans-regular.woff2 │ └── js │ │ └── scale.fix.js ├── index.html ├── screenshots │ ├── job.png │ └── overview.png └── style.css ├── package-lock.json ├── package.json ├── runtests.sh ├── saltgui ├── index.html └── static │ ├── hilitor │ └── hilitor.js │ ├── images │ ├── UNKNOWN.png │ ├── externallink.png │ ├── github.png │ ├── icon.png │ ├── os-aix.png │ ├── os-almalinux.png │ ├── os-alpine.png │ ├── os-alt.png │ ├── os-amazon.png │ ├── os-antergos.png │ ├── os-arch-arm.png │ ├── os-arch.png │ ├── os-centos-stream.png │ ├── os-centos.png │ ├── os-chapeau.png │ ├── os-cloudlinux.png │ ├── os-darwin.png │ ├── os-debian.png │ ├── os-devuan.png │ ├── os-elementary-os.png │ ├── os-elementary.png │ ├── os-fedora-asahi-remix.png │ ├── os-fedora.png │ ├── os-freebsd.png │ ├── os-gentoo.png │ ├── os-kali.png │ ├── os-kde-neon.png │ ├── os-korora.png │ ├── os-macos.png │ ├── os-mageia.png │ ├── os-manjaro.png │ ├── os-mint.png │ ├── os-netbsd.png │ ├── os-nilinuxrt-xfce.png │ ├── os-nilinuxrt.png │ ├── os-oel.png │ ├── os-openbsd.png │ ├── os-opensolaris.png │ ├── os-openwrt.png │ ├── os-oracle-solaris.png │ ├── os-raspbian.png │ ├── os-redhat.png │ ├── os-rocky.png │ ├── os-scientificlinux.png │ ├── os-slackware.png │ ├── os-smartos.png │ ├── os-solaris.png │ ├── os-steamos.png │ ├── os-suse.png │ ├── os-ubuntu.png │ ├── os-univention.png │ ├── os-vmware-photon-os.png │ ├── os-vmware.png │ ├── os-void.png │ └── os-windows.png │ ├── jsonpath │ └── jsonpath-0.8.0.js │ ├── scripts │ ├── Api.js │ ├── Character.js │ ├── CommandBox.js │ ├── Documentation.js │ ├── DropDown.js │ ├── ParseCommandLine.js │ ├── Router.js │ ├── RunType.js │ ├── TargetType.js │ ├── Utils.js │ ├── config.js │ ├── index.js │ ├── issues │ │ ├── Beacons.js │ │ ├── Issues.js │ │ ├── JobsRunning.js │ │ ├── Keys.js │ │ ├── NotConnected.js │ │ ├── Schedules.js │ │ └── State.js │ ├── output │ │ ├── Output.js │ │ ├── OutputDocumentation.js │ │ ├── OutputHighstate.js │ │ ├── OutputHighstateSummaryOriginal.js │ │ ├── OutputHighstateSummarySaltGui.js │ │ ├── OutputHighstateTaskFull.js │ │ ├── OutputHighstateTaskSaltGui.js │ │ ├── OutputHighstateTaskTerse.js │ │ ├── OutputJson.js │ │ ├── OutputNested.js │ │ └── OutputYaml.js │ ├── pages │ │ ├── Beacons.js │ │ ├── BeaconsMinion.js │ │ ├── Events.js │ │ ├── Grains.js │ │ ├── GrainsMinion.js │ │ ├── HighState.js │ │ ├── Issues.js │ │ ├── Job.js │ │ ├── Jobs.js │ │ ├── Keys.js │ │ ├── Login.js │ │ ├── Logout.js │ │ ├── Minions.js │ │ ├── Nodegroups.js │ │ ├── Options.js │ │ ├── Orchestrations.js │ │ ├── Page.js │ │ ├── Pillars.js │ │ ├── PillarsMinion.js │ │ ├── Reactors.js │ │ ├── Schedules.js │ │ ├── SchedulesMinion.js │ │ └── Templates.js │ └── panels │ │ ├── Beacons.js │ │ ├── BeaconsMinion.js │ │ ├── Events.js │ │ ├── Grains.js │ │ ├── GrainsMinion.js │ │ ├── HighState.js │ │ ├── Issues.js │ │ ├── Job.js │ │ ├── Jobs.js │ │ ├── JobsDetails.js │ │ ├── JobsSummary.js │ │ ├── Keys.js │ │ ├── Login.js │ │ ├── Minions.js │ │ ├── Nodegroups.js │ │ ├── Options.js │ │ ├── Orchestrations.js │ │ ├── Panel.js │ │ ├── Pillars.js │ │ ├── PillarsMinion.js │ │ ├── Reactors.js │ │ ├── Schedules.js │ │ ├── SchedulesMinion.js │ │ ├── Stats.js │ │ └── Templates.js │ ├── sorttable │ ├── sorttable.css │ └── sorttable.js │ └── stylesheets │ ├── beacons.css │ ├── controls.css │ ├── dropdown.css │ ├── events.css │ ├── grains.css │ ├── job.css │ ├── keys.css │ ├── login.css │ ├── main.css │ ├── options.css │ ├── page.css │ ├── pillars.css │ ├── schedules.css │ └── tooltip.css └── tests ├── functional └── login.js ├── helpers └── wait-for-docker.js └── unit ├── Output.test.js ├── ParseCommandLine.test.js ├── TargetType.test.js └── Utils.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "@babel/preset-env" ] 3 | } 4 | -------------------------------------------------------------------------------- /.codebeatignore: -------------------------------------------------------------------------------- 1 | saltgui/static/jsonpath/jsonpath-0.8.0.js 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: erwindon 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behaviour** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: erwindon 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: eslint 11 | versions: ">= 9" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '16 21 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - "master" 10 | pull_request: 11 | branches: 12 | - "master" 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [18.x, 20.x, 22.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: 'npm' 32 | 33 | - run: npm ci 34 | 35 | - run: npm run stylelint 36 | 37 | - run: npm run eslint 38 | 39 | - run: npm run test:coverage 40 | 41 | - run: | 42 | echo "--- searching for 'replaceAll' ---" 43 | CNT=`find saltgui/static/scripts -name "*.js" | xargs fgrep "replaceAll" | wc -l` 44 | if [ $CNT != 0 ]; then 45 | find saltgui/static/scripts -name "*.js" | xargs fgrep "replaceAll" 46 | echo "'replaceAll' must be replaced with 'replace(//g, ...)' due to regression test" 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .gitignore 3 | yarn-error.log 4 | node_modules/ 5 | .idea 6 | npm-debug.log 7 | yarn.lock 8 | .nyc_output/ 9 | saltgui/static/minions.txt 10 | saltgui/static/mkMinions.sh 11 | saltgui/static/salt-auth.txt* 12 | saltgui/static/salt-motd.txt* 13 | saltgui/static/salt-motd.html* 14 | *.swp 15 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | Versions 2 | -------- 3 | These docker images are build with: 4 | - CentOS Stream 9 5 | - Debian 12 (bookworm) 6 | - Ubuntu 22.04 (jammy) 7 | 8 | They use the following repositories to install saltstack: 9 | - https://repo.saltproject.io/salt/py3/redhat/9/x86_64/minor/${SALT_VERSION} 10 | - https://repo.saltproject.io/salt/py3/debian/12/amd64/minor/${SALT_VERSION} 11 | - https://repo.saltproject.io/salt/py3/ubuntu/22.04/amd64/minor/${SALT_VERSION} 12 | 13 | Version of all salt packages installed: *SALT_VERSION=3007.1* 14 | 15 | Docker images 16 | ------------- 17 | Commands used to build these docker images (you must be inside `dockerfiles` folder): 18 | 19 | ## salt master based on ubuntu 20 | ``` 21 | docker build -f dockerfile-saltmaster --tag erwindon/saltgui-saltmaster:3007.1 . 22 | ``` 23 | 24 | ## salt minion based on centos 25 | ``` 26 | docker build -f dockerfile-saltminion-centos --tag erwindon/saltgui-saltminion-centos:3007.1 . 27 | ``` 28 | 29 | ## salt minion based on debian 30 | ``` 31 | docker build -f dockerfile-saltminion-debian --tag erwindon/saltgui-saltminion-debian:3007.1 . 32 | ``` 33 | 34 | ## salt minion based on ubuntu 35 | ``` 36 | docker build -f dockerfile-saltminion-ubuntu --tag erwindon/saltgui-saltminion-ubuntu:3007.1 . 37 | ``` 38 | 39 | ## saltgui-nginx (separated) based on ubuntu 40 | ``` 41 | docker build -f dockerfile-saltgui-nginx --tag erwindon/saltgui-nginx:1.18.0 . 42 | ``` 43 | -------------------------------------------------------------------------------- /docker/build-and-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | cd dockerfiles 4 | set -e 5 | tag=3007.1 6 | docker build -f dockerfile-saltmaster --tag erwindon/saltgui-saltmaster:$tag --tag erwindon/saltgui-saltmaster:latest . 7 | docker build -f dockerfile-saltminion-ubuntu --tag erwindon/saltgui-saltminion-ubuntu:$tag --tag erwindon/saltgui-saltminion-ubuntu:latest . 8 | docker build -f dockerfile-saltminion-debian --tag erwindon/saltgui-saltminion-debian:$tag --tag erwindon/saltgui-saltminion-debian:latest . 9 | docker build -f dockerfile-saltminion-centos --tag erwindon/saltgui-saltminion-centos:$tag --tag erwindon/saltgui-saltminion-centos:latest . 10 | docker container ls -aq | xargs --no-run-if-empty docker container rm --force 11 | docker images | awk '/^/ {print $3;}' | xargs --no-run-if-empty docker rmi 12 | for t in $tag latest; do 13 | # this needs "docker login" 14 | docker push erwindon/saltgui-saltmaster:$t 15 | docker push erwindon/saltgui-saltminion-ubuntu:$t 16 | docker push erwindon/saltgui-saltminion-debian:$t 17 | docker push erwindon/saltgui-saltminion-centos:$t 18 | done 19 | docker system prune --force --filter "until=12h" 20 | docker images 21 | # End 22 | -------------------------------------------------------------------------------- /docker/conf/master: -------------------------------------------------------------------------------- 1 | # /etc/salt/master 2 | 3 | file_roots: 4 | base: 5 | - /srv/salt/ 6 | 7 | pillar_roots: 8 | base: 9 | - /srv/pillar 10 | 11 | external_auth: 12 | pam: 13 | salt: 14 | - .* 15 | - '@runner' 16 | - '@wheel' 17 | - '@jobs' 18 | 19 | rest_cherrypy: 20 | port: 3333 21 | host: 0.0.0.0 22 | disable_ssl: true 23 | app: /saltgui/index.html 24 | static: /saltgui/static 25 | static_path: /static 26 | 27 | netapi_enable_clients: 28 | - local 29 | - local_async 30 | - runner 31 | - wheel 32 | 33 | saltgui_templates: 34 | template1: 35 | description: First template 36 | target: "*" 37 | command: test.fib num=10 38 | template2: 39 | description: Second template 40 | targettype: glob 41 | target: "*ubuntu*" 42 | command: test.version 43 | template3: 44 | description: Empty template 45 | template4: 46 | description: Fourth template 47 | targettype: compound 48 | target: "G@os:Ubuntu" 49 | command: test.version 50 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | saltmaster-local: 4 | image: erwindon/saltgui-saltmaster:3007.1 5 | hostname: saltmaster-local 6 | ports: 7 | - 4505:4505 8 | - 4506:4506 9 | - 3333:3333 10 | volumes: 11 | - ./srv/:/srv/ 12 | - ./conf/master:/etc/salt/master 13 | - ../saltgui:/saltgui 14 | 15 | saltminion-ubuntu: 16 | image: erwindon/saltgui-saltminion-ubuntu:3007.1 17 | hostname: saltminion-ubuntu 18 | depends_on: 19 | - saltmaster-local 20 | restart: on-failure 21 | 22 | saltminion-debian: 23 | image: erwindon/saltgui-saltminion-debian:3007.1 24 | hostname: saltminion-debian 25 | depends_on: 26 | - saltmaster-local 27 | restart: on-failure 28 | 29 | saltminion-centos: 30 | image: erwindon/saltgui-saltminion-centos:3007.1 31 | hostname: saltminion-centos 32 | depends_on: 33 | - saltmaster-local 34 | restart: on-failure 35 | 36 | # This example will demonstrate how to use NGINX for SaltGUI and proxy to salt-api 37 | # saltgui-nginx: 38 | # image: erwindon/saltgui-nginx:1.14.0 39 | # hostname: saltgui-nginx 40 | # ports: 41 | # - 8080:80 42 | # depends_on: 43 | # - saltmaster-local 44 | # restart: on-failure 45 | # volumes: 46 | # - ../saltgui:/data/www 47 | -------------------------------------------------------------------------------- /docker/dockerfiles/conf/minion: -------------------------------------------------------------------------------- 1 | # /etc/salt/minion 2 | master: saltmaster-local 3 | -------------------------------------------------------------------------------- /docker/dockerfiles/conf/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | root /data/www; 5 | index index.html; 6 | 7 | # handle internal api (proxy) 8 | location /api/ { 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | proxy_set_header X-NginX-Proxy true; 12 | proxy_pass http://saltmaster-local:3333/; 13 | proxy_ssl_session_reuse off; 14 | proxy_set_header Host $http_host; 15 | proxy_redirect off; 16 | } 17 | 18 | # handle saltgui web page 19 | location / { 20 | try_files $uri /index.html; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /docker/dockerfiles/conf/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | charset UTF-8; 15 | 16 | log_format main '[$time_local] $remote_user:$remote_addr "$request" ' 17 | '$status $body_bytes_sent "$http_referer" ' 18 | '"$http_user_agent" "$http_x_forwarded_for"'; 19 | 20 | access_log /var/log/nginx/access.log main; 21 | sendfile on; 22 | keepalive_timeout 65; 23 | include /etc/nginx/conf.d/*.conf; 24 | } 25 | -------------------------------------------------------------------------------- /docker/dockerfiles/conf/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | loglevel=warn 5 | 6 | [program:saltmaster] 7 | command=/usr/bin/salt-master -l info 8 | 9 | [program:saltapi] 10 | command=/usr/bin/salt-api 11 | -------------------------------------------------------------------------------- /docker/dockerfiles/dockerfile-saltgui-nginx: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | LABEL maintainer="Erwin Dondorp " 4 | LABEL name=saltgui-nginx 5 | LABEL project="SaltGUI testing" 6 | LABEL version=1.18.0 7 | 8 | ENV NGINX_VERSION=1.18.0 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | RUN apt-get update \ 12 | # install nginx 13 | && apt-get install -y nginx=${NGINX_VERSION}* --no-install-recommends \ 14 | # show which versions are installed 15 | && dpkg -l | grep nginx \ 16 | # cleanup temporary files 17 | && rm -rf /var/lib/apt/lists/* \ 18 | && apt-get -y autoremove \ 19 | && apt-get clean \ 20 | # cleanup nginx 21 | && rm /etc/nginx/sites-enabled/default \ 22 | # configure docker nginx logging 23 | && ln -sf /dev/stdout /var/log/nginx/access.log \ 24 | && ln -sf /dev/stderr /var/log/nginx/error.log 25 | 26 | # copy the nginx configuration 27 | COPY ./conf/nginx/nginx.conf /etc/nginx/nginx.conf 28 | COPY ./conf/nginx/default.conf /etc/nginx/conf.d/default.conf 29 | 30 | # define main container command 31 | CMD ["nginx", "-g", "daemon off;"] 32 | -------------------------------------------------------------------------------- /docker/dockerfiles/dockerfile-saltmaster: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | LABEL maintainer="Erwin Dondorp " 4 | LABEL name=salt-master 5 | LABEL project="SaltGUI testing" 6 | LABEL version=3007.1 7 | 8 | ENV SALT_VERSION=3007.1 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | # make download possible, make encrypted password generation possible 12 | RUN apt-get update 13 | RUN apt-get install --yes --no-install-recommends curl openssl adduser 14 | 15 | # add a user for the frontend salt:salt 16 | RUN adduser salt 17 | RUN usermod -s /bin/bash -p "$(openssl passwd -1 salt)" salt 18 | 19 | # install salt-master with salt-api 20 | # not using repo, so must explicitly do all packages 21 | RUN curl -k -L -o salt-common_${SALT_VERSION}.deb https://packages.broadcom.com/artifactory/saltproject-deb/pool/salt-common_${SALT_VERSION}_amd64.deb 22 | RUN curl -k -L -o salt-api_${SALT_VERSION}.deb https://packages.broadcom.com/artifactory/saltproject-deb/pool/salt-api_${SALT_VERSION}_amd64.deb 23 | RUN curl -k -L -o salt-master_${SALT_VERSION}.deb https://packages.broadcom.com/artifactory/saltproject-deb/pool/salt-master_${SALT_VERSION}_amd64.deb 24 | RUN apt install --yes --no-install-recommends ./salt-common_${SALT_VERSION}.deb ./salt-master_${SALT_VERSION}.deb ./salt-api_${SALT_VERSION}.deb 25 | 26 | # install supervisor 27 | # becausewe need to run salt-master and salt-api 28 | RUN apt-get install --yes --no-install-recommends supervisor 29 | 30 | # cleanup temporary files 31 | RUN rm -rf /var/lib/apt/lists/* *.deb \ 32 | && apt-get --yes autoremove \ 33 | && apt-get clean 34 | 35 | # copy supervisord configuration 36 | COPY ./conf/supervisord.conf /etc/supervisor/conf.d/supervisord.conf 37 | 38 | # some volume configuration for the saltmaster 39 | VOLUME ["/pki", "/var/cache/salt", "/var/log/salt"] 40 | EXPOSE 3333 4505 4506 41 | 42 | # define main container command 43 | # explicitly mentioning the (default) configuration file saves a warning 44 | CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] 45 | -------------------------------------------------------------------------------- /docker/dockerfiles/dockerfile-saltminion-centos: -------------------------------------------------------------------------------- 1 | FROM dokken/centos-stream-9:latest 2 | 3 | LABEL maintainer="Erwin Dondorp " 4 | LABEL name=salt-minion 5 | LABEL project="SaltGUI testing" 6 | LABEL version=3007.1 7 | 8 | ENV SALT_VERSION=3007.1 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | # get saltstack software 12 | RUN yum install --assumeyes epel-release curl 13 | 14 | # install salt-minion 15 | # not using repo, so must explicitly do all packages 16 | RUN yum install --assumeyes https://packages.broadcom.com/artifactory/saltproject-rpm/salt-${SALT_VERSION}-0.x86_64.rpm 17 | RUN yum install --assumeyes https://packages.broadcom.com/artifactory/saltproject-rpm/salt-minion-${SALT_VERSION}-0.x86_64.rpm 18 | 19 | # cleanup temporary files 20 | RUN rm -rf /var/lib/yum/* /var/cache/yum *.rpm \ 21 | && yum clean all 22 | 23 | # copy the minion configuration 24 | COPY ./conf/minion /etc/salt/minion 25 | 26 | # define main container command 27 | CMD ["/usr/bin/salt-minion"] 28 | -------------------------------------------------------------------------------- /docker/dockerfiles/dockerfile-saltminion-debian: -------------------------------------------------------------------------------- 1 | FROM debian:12 2 | 3 | LABEL maintainer="Erwin Dondorp " 4 | LABEL name=salt-minion 5 | LABEL project="SaltGUI testing" 6 | LABEL version=3007.1 7 | 8 | ENV SALT_VERSION=3007.1 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | # make download possible 12 | RUN apt-get update 13 | RUN apt-get install --yes --no-install-recommends curl 14 | 15 | # install salt-minion 16 | # not using repo, so must explicitly do all packages 17 | RUN curl -k -L -o salt-common_${SALT_VERSION}.deb https://packages.broadcom.com/artifactory/saltproject-deb/pool/salt-common_${SALT_VERSION}_amd64.deb 18 | RUN curl -k -L -o salt-minion_${SALT_VERSION}.deb https://packages.broadcom.com/artifactory/saltproject-deb/pool/salt-minion_${SALT_VERSION}_amd64.deb 19 | RUN apt install --yes --no-install-recommends ./salt-common_${SALT_VERSION}.deb ./salt-minion_${SALT_VERSION}.deb 20 | 21 | # cleanup temporary files 22 | RUN rm -rf /var/lib/apt/lists/* *.deb \ 23 | && apt-get -y autoremove \ 24 | && apt-get clean 25 | 26 | # copy the minion configuration 27 | COPY ./conf/minion /etc/salt/minion 28 | 29 | ENV CRYPTOGRAPHY_OPENSSL_NO_LEGACY=true 30 | 31 | # define main container command 32 | CMD ["/usr/bin/salt-minion"] 33 | -------------------------------------------------------------------------------- /docker/dockerfiles/dockerfile-saltminion-ubuntu: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | LABEL maintainer="Erwin Dondorp " 4 | LABEL name=salt-minion 5 | LABEL project="SaltGUI testing" 6 | LABEL version=3007.1 7 | 8 | ENV SALT_VERSION=3007.1 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | # make download possible 12 | RUN apt-get update 13 | RUN apt-get install --yes --no-install-recommends curl 14 | 15 | # install salt-minion 16 | # not using repo, so must explicitly do all packages 17 | RUN curl -k -L -o salt-common_${SALT_VERSION}.deb https://packages.broadcom.com/artifactory/saltproject-deb/pool/salt-common_${SALT_VERSION}_amd64.deb 18 | RUN curl -k -L -o salt-minion_${SALT_VERSION}.deb https://packages.broadcom.com/artifactory/saltproject-deb/pool/salt-minion_${SALT_VERSION}_amd64.deb 19 | RUN apt install --yes --no-install-recommends ./salt-common_${SALT_VERSION}.deb ./salt-minion_${SALT_VERSION}.deb 20 | 21 | # cleanup temporary files 22 | RUN rm -rf /var/lib/apt/lists/* *.deb \ 23 | && apt-get -y autoremove \ 24 | && apt-get clean 25 | 26 | # copy the minion configuration 27 | COPY ./conf/minion /etc/salt/minion 28 | 29 | ENV CRYPTOGRAPHY_OPENSSL_NO_LEGACY=true 30 | 31 | # define main container command 32 | CMD ["/usr/bin/salt-minion"] 33 | -------------------------------------------------------------------------------- /docker/srv/pillar/secrets.sls: -------------------------------------------------------------------------------- 1 | # don't bother: the password is not real 2 | password: dq4wh7nRTNq 3 | -------------------------------------------------------------------------------- /docker/srv/pillar/top.sls: -------------------------------------------------------------------------------- 1 | base: 2 | '*': 3 | - secrets 4 | -------------------------------------------------------------------------------- /docker/srv/salt/top.sls: -------------------------------------------------------------------------------- 1 | base: 2 | '*': 3 | - vim 4 | -------------------------------------------------------------------------------- /docker/srv/salt/vim/init.sls: -------------------------------------------------------------------------------- 1 | {% from "vim/map.jinja" import vim with context %} 2 | 3 | install vim package : 4 | pkg.installed: 5 | - pkgs: 6 | - {{ vim.package }} 7 | -------------------------------------------------------------------------------- /docker/srv/salt/vim/map.jinja: -------------------------------------------------------------------------------- 1 | {% set vim = salt['grains.filter_by']({ 2 | 'default': { 3 | 'package': 'vim', 4 | }, 5 | 'RedHat': { 6 | 'package': 'vim-enhanced', 7 | } 8 | }) %} 9 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions to the SaltGUI project are welcome!
4 | This can either be through a pull-request (PR) or by creating an issue with an idea. 5 | 6 | When you create an issue with an idea, make sure that: 7 | * it is one idea/question at a time 8 | * it is useful for most users 9 | * new `salt-api` calls are identified in the request 10 | 11 | There are few coding standards: 12 | * obey the rules from stylelint (for CSS) 13 | * obey the rules from eslint (for JS) 14 | * local functionnames start with "_" 15 | 16 | When you create a PR, make sure that: 17 | * it is one idea at a time 18 | * it is as simple as possible 19 | * it is useful for most users 20 | * there are no errors from `eslint` 21 | * there are no errors from `stylelint` 22 | * there are coverage tests to cover all new non-gui code 23 | * there are functional/gui tests to cover all new gui code 24 | -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018-2023 Erwin Dondorp
5 | Copyright (c) 2017-2018 Martijn Jacobs
6 | Copyright (c) 2016-2017 Oliver Dunk 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | -------------------------------------------------------------------------------- /docs/PERMISSIONS.md: -------------------------------------------------------------------------------- 1 | # Permission settings 2 | 3 | What users can do with SaltGUI is configured in salt using the `external_auth` configuration directive. 4 | See the [EAUTH](https://docs.saltstack.com/en/latest/topics/eauth/index.html) documentation for more information. 5 | 6 | ## Alternative configuration 7 | The default configuration from the quickstart allows all commands. 8 | Here is an example with a more detailed configuration: 9 | ``` 10 | external_auth: 11 | pam: 12 | saltuser: # the unix username which is allowed to login 13 | - .* # allow to execute all modules 14 | 15 | - '@jobs' # allows acccess to the `/jobs` rest api 16 | - '@runner': 17 | - 'jobs.*' # allows the job runner function to determine if jobs are running 18 | # but no other runner commands 19 | - '@wheel': 20 | - 'key.*' # allows keys management and listing 21 | # but no other wheel commands 22 | - 'config.values' 23 | ``` 24 | 25 | So this is a basic configuration which allows some of the basic functionality SaltGUI has to offer. 26 | Resticting access to modules can be simply done by replacing a wildcard and specifiying explicit details like this: 27 | ``` 28 | ... 29 | - grains.items 30 | - sys.doc 31 | - state.apply 32 | - cmd.* 33 | ... 34 | ``` 35 | 36 | ## Minimum permission settings 37 | 38 | The following configuration is a mimimum set of permissions, so that SaltGUI can populate its screens: 39 | ``` 40 | - beacons.list 41 | - grains.items 42 | - pillar.items 43 | - pillar.obfuscate 44 | - schedule.list 45 | - '@runner': 46 | - jobs.active 47 | - jobs.list_job 48 | - jobs.list_jobs 49 | - manage.versions 50 | - '@wheel': 51 | - config.values 52 | - key.finger 53 | - key.list_all 54 | - minions.connected 55 | ``` 56 | Adititional permissions are needed to run the commands associated with the popupmenu items. 57 | These commands are clearly visible in the gui, and are not listed here. 58 | 59 | SaltGUI is designed to cope with any API failure, whether due to authorization issues, or due to technical issues. 60 | Please report any deviations from that. 61 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - CHANGELOG.md 3 | - CONTRIBUTING.md 4 | - LICENSE.md 5 | - PERMISSIONS.md 6 | - README.md 7 | -------------------------------------------------------------------------------- /docs/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/docs/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/assets/fonts/Noto-Sans-regular/Noto-Sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/docs/assets/fonts/Noto-Sans-regular/Noto-Sans-regular.woff2 -------------------------------------------------------------------------------- /docs/assets/js/scale.fix.js: -------------------------------------------------------------------------------- 1 | (function(document) { 2 | var metas = document.getElementsByTagName('meta'), 3 | changeViewportContent = function(content) { 4 | for (var i = 0; i < metas.length; i++) { 5 | if (metas[i].name == "viewport") { 6 | metas[i].content = content; 7 | } 8 | } 9 | }, 10 | initialize = function() { 11 | changeViewportContent("width=device-width, minimum-scale=1.0, maximum-scale=1.0"); 12 | }, 13 | gestureStart = function() { 14 | changeViewportContent("width=device-width, minimum-scale=0.25, maximum-scale=1.6"); 15 | }, 16 | gestureEnd = function() { 17 | initialize(); 18 | }; 19 | 20 | 21 | if (navigator.userAgent.match(/iPhone/i)) { 22 | initialize(); 23 | 24 | document.addEventListener("touchstart", gestureStart, false); 25 | document.addEventListener("touchend", gestureEnd, false); 26 | } 27 | })(document); 28 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SaltGUI - documentation 5 | 6 | 7 | 39 | 40 | 41 |
42 |
43 |

SaltGUI

44 |

A web interface for managing SaltStack based infrastructure.

45 |

View the Project on GitHub erwindon/SaltGUI

46 | 47 |
48 | 49 |
50 |
51 | 52 | 56 |
57 | 58 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/screenshots/job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/docs/screenshots/job.png -------------------------------------------------------------------------------- /docs/screenshots/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/docs/screenshots/overview.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saltgui-test", 3 | "description": "Code linting and testing package for SaltGUI", 4 | "author": "Martijn Jacobs", 5 | "version": "0.0.1", 6 | "license": "MIT", 7 | "private": true, 8 | "type": "module", 9 | "devDependencies": { 10 | "@babel/core": "^7.27.4", 11 | "@babel/preset-env": "^7.27.2", 12 | "@babel/register": "^7.27.1", 13 | "babel-plugin-istanbul": "^7.0.0", 14 | "chai": "^5.2.0", 15 | "eslint": "^8.57.1", 16 | "eslint-plugin-compat": "^6.0.2", 17 | "mocha": "^11.5.0", 18 | "nightmare": "^3.0.2", 19 | "nyc": "^17.1.0", 20 | "stylelint": "^14.0.1", 21 | "stylelint-config-standard": "^29.0.0", 22 | "stylelint-csstree-validator": "^2.0.0", 23 | "stylelint-scss": "^5.3.2" 24 | }, 25 | "eslintConfig": { 26 | "env": { 27 | "es6": true 28 | }, 29 | "extends": [ 30 | "eslint:all", 31 | "plugin:compat/recommended" 32 | ], 33 | "parserOptions": { 34 | "ecmaVersion": 6, 35 | "sourceType": "module" 36 | }, 37 | "rules": { 38 | "array-element-newline": 0, 39 | "capitalized-comments": 0, 40 | "class-methods-use-this": 2, 41 | "complexity": 0, 42 | "dot-notation": 0, 43 | "function-call-argument-newline": 0, 44 | "function-paren-newline": 0, 45 | "guard-for-in": 0, 46 | "id-length": [ 47 | "error", 48 | { 49 | "exceptions": [ 50 | "i" 51 | ] 52 | } 53 | ], 54 | "indent": [ 55 | "error", 56 | 2 57 | ], 58 | "init-declarations": 0, 59 | "max-classes-per-file": 0, 60 | "max-len": 0, 61 | "max-lines": 0, 62 | "max-lines-per-function": 0, 63 | "max-params": 0, 64 | "max-statements": 0, 65 | "multiline-comment-style": 0, 66 | "multiline-ternary": 0, 67 | "no-console": 0, 68 | "no-continue": 0, 69 | "no-invalid-this": 2, 70 | "no-loss-of-precision": 0, 71 | "no-magic-numbers": 0, 72 | "no-mixed-operators": 0, 73 | "no-param-reassign": 0, 74 | "no-plusplus": [ 75 | "error", 76 | { 77 | "allowForLoopAfterthoughts": true 78 | } 79 | ], 80 | "no-redeclare": 2, 81 | "no-ternary": 0, 82 | "no-undef-init": 2, 83 | "no-undefined": 0, 84 | "no-underscore-dangle": 0, 85 | "no-unused-vars": [ 86 | "error", 87 | { 88 | "args": "all" 89 | } 90 | ], 91 | "no-warning-comments": 0, 92 | "object-property-newline": 0, 93 | "object-shorthand": 0, 94 | "one-var": 0, 95 | "padded-blocks": 0, 96 | "prefer-destructuring": 0, 97 | "prefer-named-capture-group": 0, 98 | "prefer-template": 0, 99 | "require-unicode-regexp": 0 100 | } 101 | }, 102 | "stylelint": { 103 | "extends": "stylelint-config-standard", 104 | "rules": { 105 | "property-no-vendor-prefix": null, 106 | "color-function-notation": null 107 | } 108 | }, 109 | "nyc": { 110 | "check-coverage": false, 111 | "per-file": true, 112 | "lines": 85, 113 | "statements": 85, 114 | "functions": 85, 115 | "branches": 85, 116 | "cache": true, 117 | "all": true, 118 | "watermarks": { 119 | "lines": [ 120 | 70, 121 | 90 122 | ], 123 | "functions": [ 124 | 70, 125 | 90 126 | ], 127 | "branches": [ 128 | 70, 129 | 90 130 | ], 131 | "statements": [ 132 | 70, 133 | 90 134 | ] 135 | }, 136 | "exclude": [ 137 | "tests/**/*.js", 138 | "node_modules/**/*.js" 139 | ], 140 | "include": [ 141 | "saltgui/static/scripts/**/*.js" 142 | ] 143 | }, 144 | "scripts": { 145 | "eslint": "eslint saltgui/static/scripts tests/", 146 | "eslint:fix": "eslint saltgui/static/scripts tests/ --fix", 147 | "stylelint": "stylelint 'saltgui/static/stylesheets/*.css'", 148 | "wait-for-docker": "node tests/helpers/wait-for-docker.js", 149 | "test:unit": "mocha --require @babel/register --trace-warnings --check-leaks --reporter spec tests/unit/", 150 | "test:functional": "mocha --bail --trace-warnings --check-leaks --reporter spec tests/functional/", 151 | "test:coverage": "nyc mocha --require @babel/register --trace-warnings --check-leaks --reporter spec tests/unit/" 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # show what is going on 4 | set -x 5 | 6 | # get the needed software 7 | sudo apt install -y libxss1 libgconf-2-4 libnss3 libasound2 xvfb psmisc 8 | 9 | # prevent conflict with a running salt installation 10 | sudo systemctl stop salt-master salt-api 11 | # or a previous running xvfb 12 | killall Xvfb 13 | 14 | set -e 15 | 16 | # add testing packages 17 | npm install 18 | 19 | # first see if we write es6 compatible js 20 | npm run eslint 21 | 22 | # and if our css is sane 23 | npm run stylelint 24 | 25 | # run the unittests tests before docker for failing fast 26 | # the coverage test will repeat all this 27 | #npm run test:unit 28 | 29 | # run the unittests tests and create coverage report 30 | npm run test:coverage 31 | 32 | # start a salt master, three salt minions and saltgui to run tests on 33 | # Don't use --detach; travis docker does not understand it 34 | docker-compose --file docker/docker-compose.yml up -d 35 | 36 | # wait until all are up 37 | npm run wait-for-docker 38 | 39 | # run the nightmare.js functional tests 40 | # when debugging is needed: 41 | #export DEBUG=nightmare:*,electron:* 42 | #export NIGHTMARE_DEBUG=1 43 | # suppress Electron Security Warnings: 44 | export ELECTRON_DISABLE_SECURITY_WARNINGS=true 45 | xvfb-run npm run test:functional 46 | 47 | # remove the containers 48 | docker-compose --file docker/docker-compose.yml rm --force --stop 49 | 50 | # start the usual software again 51 | sudo systemctl start salt-master salt-api 52 | 53 | echo "DONE!" 54 | -------------------------------------------------------------------------------- /saltgui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SaltGUI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 60 | 61 |
62 |
63 | 64 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 93 | 94 | 95 | 102 | 103 | 104 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /saltgui/static/hilitor/hilitor.js: -------------------------------------------------------------------------------- 1 | // Original JavaScript code by Chirp Internet: www.chirp.com.au 2 | // Please acknowledge use of this code by including this header. 3 | 4 | //function Hilitor(id, tag) 5 | function Hilitor(start, id, tag) 6 | { 7 | 8 | // private variables 9 | //var targetNode = document.getElementById(id) || document.body; 10 | let targetNode = start; 11 | if(id) targetNode = targetNode.querySelector(id); 12 | var hiliteTag = tag || "MARK"; 13 | var skipTags = new RegExp("^(?:" + hiliteTag + "|SCRIPT|FORM)$"); 14 | var colors = ["#ff6", "#a0ffff", "#9f9", "#f99", "#f6f"]; 15 | var wordColor = []; 16 | var colorIdx = 0; 17 | var matchRegExp = ""; 18 | var openLeft = false; 19 | var openRight = false; 20 | 21 | var nrHilites = 0; 22 | 23 | // characters to strip from start and end of the input string 24 | var endRegExp = /^[^\\w]+|[^\\w]+$/g; 25 | 26 | // characters used to break up the input string into words 27 | var breakRegExp = /[^\\w\'-]+/g; 28 | 29 | this.setEndRegExp = function(regex) { 30 | endRegExp = regex; 31 | return endRegExp; 32 | }; 33 | 34 | this.setBreakRegExp = function(regex) { 35 | breakRegExp = regex; 36 | return breakRegExp; 37 | }; 38 | 39 | this.setMatchType = function(type) 40 | { 41 | switch(type) 42 | { 43 | case "left": 44 | this.openLeft = false; 45 | this.openRight = true; 46 | break; 47 | 48 | case "right": 49 | this.openLeft = true; 50 | this.openRight = false; 51 | break; 52 | 53 | case "open": 54 | this.openLeft = this.openRight = true; 55 | break; 56 | 57 | default: 58 | this.openLeft = this.openRight = false; 59 | 60 | } 61 | }; 62 | 63 | this.setRegex = function(input, isCaseSensitive=false) 64 | { 65 | input = input.replace(endRegExp, ""); 66 | input = input.replace(breakRegExp, "|"); 67 | input = input.replace(/^\||\|$/g, ""); 68 | if(input) { 69 | var re = "(?:" + input + ")"; 70 | if(!this.openLeft) re = "\\b" + re; 71 | if(!this.openRight) re = re + "\\b"; 72 | matchRegExp = new RegExp(re, isCaseSensitive ? "" : "i"); 73 | return matchRegExp; 74 | } 75 | return false; 76 | }; 77 | 78 | this.getRegex = function() 79 | { 80 | var retval = matchRegExp.toString(); 81 | retval = retval.replace(/(^\/(\\b)?|\(|\)|(\\b)?\/i$)/g, ""); 82 | retval = retval.replace(/\|/g, " "); 83 | return retval; 84 | }; 85 | 86 | // recursively apply word highlighting 87 | this.hiliteWords = function(node, isCaseSensitive=false) 88 | { 89 | if(node === undefined || !node) return; 90 | if(!matchRegExp) return; 91 | if(skipTags.test(node.nodeName)) return; 92 | 93 | // don't highlight where we don't want it 94 | if(node.classList && node.classList.contains("no-search")) return; 95 | 96 | if(node.hasChildNodes()) { 97 | for(const childNode of node.childNodes) 98 | this.hiliteWords(childNode, isCaseSensitive); 99 | } 100 | if(node.nodeType === 3) { // NODE_TEXT 101 | // limit the number of highlighted matches to 25 otherwise the DOM grows rediculously 102 | // and performance drops with it. and it is still a good first indication. 103 | let nv, regs; 104 | if(this.nrHilites <= 25 && (nv = node.nodeValue) && (regs = matchRegExp.exec(nv)) && regs[0].length > 0) { 105 | const found = isCaseSensitive ? regs[0] : regs[0].toLowerCase(); 106 | if(!wordColor[found]) { 107 | wordColor[found] = colors[colorIdx++ % colors.length]; 108 | } 109 | 110 | var match = document.createElement(hiliteTag); 111 | match.appendChild(document.createTextNode(regs[0])); 112 | match.style.backgroundColor = wordColor[found]; 113 | match.style.color = "#000"; 114 | 115 | var after = node.splitText(regs.index); 116 | after.nodeValue = after.nodeValue.substring(found.length); 117 | node.parentNode.insertBefore(match, after); 118 | 119 | this.nrHilites++; 120 | } 121 | } 122 | }; 123 | 124 | // remove highlighting 125 | this.remove = function() 126 | { 127 | var arr = targetNode.getElementsByTagName(hiliteTag); 128 | let el; 129 | while(arr.length && (el = arr[0])) { 130 | var parent = el.parentNode; 131 | parent.replaceChild(el.firstChild, el); 132 | parent.normalize(); 133 | } 134 | this.nrHilites = 0; 135 | }; 136 | 137 | // start highlighting at target node 138 | this.apply = function(input, isCaseSensitive=false) 139 | { 140 | this.remove(); 141 | if(input === undefined || !input) return undefined; 142 | if(this.setRegex(input, isCaseSensitive)) { 143 | this.hiliteWords(targetNode, isCaseSensitive); 144 | } 145 | return matchRegExp; 146 | }; 147 | 148 | } 149 | -------------------------------------------------------------------------------- /saltgui/static/images/UNKNOWN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/UNKNOWN.png -------------------------------------------------------------------------------- /saltgui/static/images/externallink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/externallink.png -------------------------------------------------------------------------------- /saltgui/static/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/github.png -------------------------------------------------------------------------------- /saltgui/static/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/icon.png -------------------------------------------------------------------------------- /saltgui/static/images/os-aix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-aix.png -------------------------------------------------------------------------------- /saltgui/static/images/os-almalinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-almalinux.png -------------------------------------------------------------------------------- /saltgui/static/images/os-alpine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-alpine.png -------------------------------------------------------------------------------- /saltgui/static/images/os-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-alt.png -------------------------------------------------------------------------------- /saltgui/static/images/os-amazon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-amazon.png -------------------------------------------------------------------------------- /saltgui/static/images/os-antergos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-antergos.png -------------------------------------------------------------------------------- /saltgui/static/images/os-arch-arm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-arch-arm.png -------------------------------------------------------------------------------- /saltgui/static/images/os-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-arch.png -------------------------------------------------------------------------------- /saltgui/static/images/os-centos-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-centos-stream.png -------------------------------------------------------------------------------- /saltgui/static/images/os-centos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-centos.png -------------------------------------------------------------------------------- /saltgui/static/images/os-chapeau.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-chapeau.png -------------------------------------------------------------------------------- /saltgui/static/images/os-cloudlinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-cloudlinux.png -------------------------------------------------------------------------------- /saltgui/static/images/os-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-darwin.png -------------------------------------------------------------------------------- /saltgui/static/images/os-debian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-debian.png -------------------------------------------------------------------------------- /saltgui/static/images/os-devuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-devuan.png -------------------------------------------------------------------------------- /saltgui/static/images/os-elementary-os.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-elementary-os.png -------------------------------------------------------------------------------- /saltgui/static/images/os-elementary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-elementary.png -------------------------------------------------------------------------------- /saltgui/static/images/os-fedora-asahi-remix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-fedora-asahi-remix.png -------------------------------------------------------------------------------- /saltgui/static/images/os-fedora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-fedora.png -------------------------------------------------------------------------------- /saltgui/static/images/os-freebsd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-freebsd.png -------------------------------------------------------------------------------- /saltgui/static/images/os-gentoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-gentoo.png -------------------------------------------------------------------------------- /saltgui/static/images/os-kali.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-kali.png -------------------------------------------------------------------------------- /saltgui/static/images/os-kde-neon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-kde-neon.png -------------------------------------------------------------------------------- /saltgui/static/images/os-korora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-korora.png -------------------------------------------------------------------------------- /saltgui/static/images/os-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-macos.png -------------------------------------------------------------------------------- /saltgui/static/images/os-mageia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-mageia.png -------------------------------------------------------------------------------- /saltgui/static/images/os-manjaro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-manjaro.png -------------------------------------------------------------------------------- /saltgui/static/images/os-mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-mint.png -------------------------------------------------------------------------------- /saltgui/static/images/os-netbsd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-netbsd.png -------------------------------------------------------------------------------- /saltgui/static/images/os-nilinuxrt-xfce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-nilinuxrt-xfce.png -------------------------------------------------------------------------------- /saltgui/static/images/os-nilinuxrt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-nilinuxrt.png -------------------------------------------------------------------------------- /saltgui/static/images/os-oel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-oel.png -------------------------------------------------------------------------------- /saltgui/static/images/os-openbsd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-openbsd.png -------------------------------------------------------------------------------- /saltgui/static/images/os-opensolaris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-opensolaris.png -------------------------------------------------------------------------------- /saltgui/static/images/os-openwrt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-openwrt.png -------------------------------------------------------------------------------- /saltgui/static/images/os-oracle-solaris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-oracle-solaris.png -------------------------------------------------------------------------------- /saltgui/static/images/os-raspbian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-raspbian.png -------------------------------------------------------------------------------- /saltgui/static/images/os-redhat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-redhat.png -------------------------------------------------------------------------------- /saltgui/static/images/os-rocky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-rocky.png -------------------------------------------------------------------------------- /saltgui/static/images/os-scientificlinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-scientificlinux.png -------------------------------------------------------------------------------- /saltgui/static/images/os-slackware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-slackware.png -------------------------------------------------------------------------------- /saltgui/static/images/os-smartos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-smartos.png -------------------------------------------------------------------------------- /saltgui/static/images/os-solaris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-solaris.png -------------------------------------------------------------------------------- /saltgui/static/images/os-steamos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-steamos.png -------------------------------------------------------------------------------- /saltgui/static/images/os-suse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-suse.png -------------------------------------------------------------------------------- /saltgui/static/images/os-ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-ubuntu.png -------------------------------------------------------------------------------- /saltgui/static/images/os-univention.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-univention.png -------------------------------------------------------------------------------- /saltgui/static/images/os-vmware-photon-os.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-vmware-photon-os.png -------------------------------------------------------------------------------- /saltgui/static/images/os-vmware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-vmware.png -------------------------------------------------------------------------------- /saltgui/static/images/os-void.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-void.png -------------------------------------------------------------------------------- /saltgui/static/images/os-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erwindon/SaltGUI/4a628471afe5f1518561ab8efd57cf245e75c57a/saltgui/static/images/os-windows.png -------------------------------------------------------------------------------- /saltgui/static/jsonpath/jsonpath-0.8.0.js: -------------------------------------------------------------------------------- 1 | /* JSONPath 0.8.0 - XPath for JSON 2 | * 3 | * Copyright (c) 2007 Stefan Goessner (goessner.net) 4 | * Licensed under the MIT (MIT-LICENSE.txt) licence. 5 | */ 6 | function jsonPath(obj, expr, arg) { 7 | var P = { 8 | resultType: arg && arg.resultType || "VALUE", 9 | result: [], 10 | normalize: function(expr) { 11 | var subx = []; 12 | return expr.replace(/[\['](\??\(.*?\))[\]']/g, function($0,$1){return "[#"+(subx.push($1)-1)+"]";}) 13 | .replace(/'?\.'?|\['?/g, ";") 14 | .replace(/;;;|;;/g, ";..;") 15 | .replace(/;$|'?\]|'$/g, "") 16 | .replace(/#(\d+)/g, function($0,$1){return subx[$1];}); 17 | }, 18 | asPath: function(path) { 19 | var x = path.split(";"), p = "$"; 20 | for (var i=1,n=x.length; i " + txt + " "; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /saltgui/static/scripts/RunType.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Character} from "./Character.js"; 4 | import {DropDownMenu} from "./DropDown.js"; 5 | import {Utils} from "./Utils.js"; 6 | 7 | export class RunType { 8 | 9 | static createMenu () { 10 | const runblock = document.getElementById("run-block"); 11 | RunType.menuRunType = new DropDownMenu(runblock); 12 | // do not show the menu title at first 13 | RunType.menuRunType.setTitle(""); 14 | RunType.menuRunType.addMenuItem("Normal", RunType._updateRunTypeText, "normal"); 15 | RunType.menuRunType.addMenuItem("Async", RunType._updateRunTypeText, "async"); 16 | RunType._updateRunTypeText(); 17 | } 18 | 19 | static _updateRunTypeText () { 20 | const runType = RunType.getRunType(); 21 | 22 | switch (runType) { 23 | case "normal": 24 | // now that the menu is used show the menu title 25 | RunType.menuRunType.setTitle("Normal"); 26 | break; 27 | case "async": 28 | RunType.menuRunType.setTitle("Async"); 29 | break; 30 | default: 31 | Utils.error("runType", runType); 32 | } 33 | 34 | // Store last used runType 35 | Utils.setStorageItem("local", "runtype", runType); 36 | 37 | const menuItems = RunType.menuRunType.menuDropdownContent.children; 38 | for (const menuItem of menuItems) { 39 | let menuItemText = menuItem.innerText; 40 | menuItemText = menuItemText.replace(/^. /, ""); 41 | if (menuItem._value === runType) { 42 | menuItemText = Character.BLACK_CIRCLE + " " + menuItemText; 43 | } 44 | menuItem.innerText = menuItemText; 45 | } 46 | } 47 | 48 | static setRunTypeDefault () { 49 | // Retrieve last used runType 50 | let runType = Utils.getStorageItem("local", "runtype"); 51 | // Set default if previous runtype not stored 52 | if (runType !== "normal" && runType !== "async") { 53 | runType = "normal"; 54 | } 55 | RunType._updateRunTypeText(); 56 | // reset the title to the absolute minimum 57 | // so that the menu does not stand out in trivial situations 58 | RunType.menuRunType.setTitle(""); 59 | } 60 | 61 | static getRunType () { 62 | let runType = RunType.menuRunType._value; 63 | if (runType === undefined || runType === "") { 64 | runType = Utils.getStorageItem("local", "runtype", "normal"); 65 | } 66 | return runType; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /saltgui/static/scripts/TargetType.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Character} from "./Character.js"; 4 | import {DropDownMenu} from "./DropDown.js"; 5 | import {Utils} from "./Utils.js"; 6 | 7 | export class TargetType { 8 | 9 | static createMenu () { 10 | const targetbox = document.getElementById("target-box"); 11 | TargetType.menuTargetType = new DropDownMenu(targetbox); 12 | // do not show the menu title at first 13 | TargetType.menuTargetType.addMenuItem("Normal", TargetType._manualUpdateTargetTypeText, "glob"); 14 | TargetType.menuTargetType.addMenuItem("List", TargetType._manualUpdateTargetTypeText, "list"); 15 | TargetType.menuTargetType.addMenuItem(TargetType._targetTypeNodeGroupPrepare, TargetType._manualUpdateTargetTypeText, "nodegroup"); 16 | TargetType.menuTargetType.addMenuItem("Compound", TargetType._manualUpdateTargetTypeText, "compound"); 17 | TargetType.setTargetTypeDefault(); 18 | } 19 | 20 | // It takes a while before we known the list of nodegroups 21 | // so this conclusion must be re-evaluated each time 22 | static _targetTypeNodeGroupPrepare (pMenuItem) { 23 | const nodeGroups = Utils.getStorageItemObject("session", "nodegroups"); 24 | if (!nodeGroups || Object.keys(nodeGroups).length === 0) { 25 | return null; 26 | } 27 | 28 | // optimization as the list of nodegroups will not change until the next login 29 | // but mainly to preserve the highlight marker 30 | pMenuItem.verifyCallBack = null; 31 | 32 | return "Nodegroup"; 33 | } 34 | 35 | static _manualUpdateTargetTypeText () { 36 | TargetType.menuTargetType._system = false; 37 | TargetType._updateTargetTypeText(); 38 | } 39 | 40 | static setTargetTypeDefault () { 41 | TargetType.menuTargetType._system = true; 42 | TargetType.menuTargetType._value = "glob"; 43 | TargetType._updateTargetTypeText(); 44 | } 45 | 46 | static _updateTargetTypeText () { 47 | const targetType = TargetType._getTargetType(); 48 | 49 | switch (targetType) { 50 | case "compound": 51 | TargetType.menuTargetType.setTitle("Compound"); 52 | break; 53 | case "glob": 54 | if (TargetType.menuTargetType._system) { 55 | // reset the title to the absolute minimum 56 | // so that the menu does not stand out in trivial situations 57 | TargetType.menuTargetType.setTitle(""); 58 | } else { 59 | TargetType.menuTargetType.setTitle("Normal"); 60 | } 61 | break; 62 | case "list": 63 | TargetType.menuTargetType.setTitle("List"); 64 | break; 65 | case "nodegroup": 66 | TargetType.menuTargetType.setTitle("Nodegroup"); 67 | break; 68 | default: 69 | Utils.error("targetType", targetType); 70 | } 71 | 72 | TargetType.menuTargetType._value = targetType; 73 | 74 | TargetType._setMenuMarker(); 75 | } 76 | 77 | static _setMenuMarker () { 78 | const targetType = TargetType._getTargetType(); 79 | const menuItems = TargetType.menuTargetType.menuDropdownContent.children; 80 | for (const menuItem of menuItems) { 81 | let menuItemText = menuItem.innerText; 82 | menuItemText = menuItemText.replace(/^. /, ""); 83 | if (menuItem._value === targetType) { 84 | menuItemText = Character.BLACK_CIRCLE + " " + menuItemText; 85 | } 86 | menuItem.innerText = menuItemText; 87 | } 88 | } 89 | 90 | static getTargetTypeFromTarget (pTarget) { 91 | if (Array.isArray(pTarget)) { 92 | return "list"; 93 | } 94 | if (pTarget.includes("@") || pTarget.includes(" ") || 95 | pTarget.includes("(") || pTarget.includes(")")) { 96 | // "@" is a strong indicator for compound target 97 | // but "space", "(" and ")" are also typical for compound target 98 | return "compound"; 99 | } 100 | if (pTarget.includes(",")) { 101 | // "," is a strong indicator for list target (when it is also not compound) 102 | return "list"; 103 | } 104 | if (pTarget.startsWith("#")) { 105 | // "#" at the start of a line is a strong indicator for nodegroup target 106 | // this is not a SALTSTACK standard, but our own invention 107 | return "nodegroup"; 108 | } 109 | return "glob"; 110 | } 111 | 112 | static autoSelectTargetType (pTarget) { 113 | 114 | if (!TargetType.menuTargetType._system) { 115 | // user has selected the value, do not touch it 116 | return; 117 | } 118 | 119 | const targetType = TargetType.getTargetTypeFromTarget (pTarget); 120 | TargetType.menuTargetType._value = targetType; 121 | 122 | // show the new title 123 | TargetType._updateTargetTypeText(); 124 | } 125 | 126 | static setTargetType (pTargetType) { 127 | TargetType.menuTargetType._value = pTargetType; 128 | TargetType.menuTargetType._system = true; 129 | TargetType._updateTargetTypeText(); 130 | } 131 | 132 | static _getTargetType () { 133 | const targetType = TargetType.menuTargetType._value; 134 | if (targetType === undefined || targetType === "") { 135 | return "glob"; 136 | } 137 | return targetType; 138 | } 139 | 140 | static makeTargetText (pObj) { 141 | const targetType = pObj["Target-type"]; 142 | const targetPattern = pObj.Target; 143 | 144 | // note that "glob" is the most common case 145 | // when used from the command-line, that target-type 146 | // is not even specified. 147 | // therefore we suppress that one 148 | 149 | // note that due to bug in 2018.3, all finished jobs 150 | // will be shown as if of type "list" 151 | // therefore we suppress that one 152 | 153 | let returnText = ""; 154 | if (targetType !== "glob" && targetType !== "list") { 155 | returnText = targetType + " "; 156 | } 157 | returnText += targetPattern; 158 | return returnText; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /saltgui/static/scripts/config.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | /* eslint-disable no-unused-vars */ 3 | const config = { 4 | // additional prefix for the API urls 5 | // with an empty string, the defaults will be used 6 | // See also https://docs.saltproject.io/en/latest/ref/netapi/all/salt.netapi.rest_cherrypy.html 7 | "API_URL": "", 8 | 9 | // additional prefix for the navigation urls 10 | // with an empty string, the defaults will be used 11 | "NAV_URL": "" 12 | }; 13 | /* eslint-enable no-unused-vars */ 14 | -------------------------------------------------------------------------------- /saltgui/static/scripts/index.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | /* istanbul ignore file */ 4 | import {Router} from "./Router.js"; 5 | window.addEventListener("load", () => new Router()); 6 | 7 | /* eslint-disable func-names */ 8 | // Make sure the errors are shown during regression testing 9 | window.onerror = function (msg, url, lineNo, columnNo, error) { 10 | console.log("JS Error:" + msg + ",error:" + error + ",url:" + url + "@" + lineNo + ":" + columnNo); 11 | if (error && error.stack) { 12 | console.log("Stack:" + error.stack); 13 | } 14 | return false; 15 | }; 16 | 17 | // simple polyfill solution 18 | if (!Object.fromEntries) { 19 | Object.fromEntries = function (pairs) { 20 | const obj = {}; 21 | for (const pair of pairs) { 22 | obj[pair[0]] = pair[1]; 23 | } 24 | return obj; 25 | } 26 | } 27 | /* eslint-enable func-names */ 28 | -------------------------------------------------------------------------------- /saltgui/static/scripts/issues/Beacons.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Issues} from "./Issues.js"; 4 | 5 | export class BeaconsIssues extends Issues { 6 | 7 | onGetIssues (pPanel) { 8 | 9 | const msg = super.onGetIssues(pPanel, "BEACONS"); 10 | 11 | const localBeaconsListPromise = this.api.getLocalBeaconsList(null); 12 | 13 | localBeaconsListPromise.then((pLocalBeaconsListData) => { 14 | Issues.removeCategory(pPanel, "disabled-beacons"); 15 | Issues.removeCategory(pPanel, "disabled-beacon"); 16 | BeaconsIssues._handleLocalBeaconsList(pPanel, pLocalBeaconsListData); 17 | Issues.readyCategory(pPanel, msg); 18 | return true; 19 | }, (pLocalBeaconsListMsg) => { 20 | Issues.removeCategory(pPanel, "disabled-beacons"); 21 | const tr1 = Issues.addIssue(pPanel, "disabled-beacons", "retrieving"); 22 | Issues.addIssueMsg(tr1, "Could not retrieve list of beacons"); 23 | Issues.addIssueErr(tr1, pLocalBeaconsListMsg); 24 | Issues.removeCategory(pPanel, "disabled-beacon"); 25 | const tr2 = Issues.addIssue(pPanel, "disabled-beacon", "retrieving"); 26 | Issues.addIssueMsg(tr2, "Could not retrieve list of beacon"); 27 | Issues.addIssueErr(tr2, pLocalBeaconsListMsg); 28 | Issues.readyCategory(pPanel, msg); 29 | return false; 30 | }); 31 | 32 | return localBeaconsListPromise; 33 | } 34 | 35 | static simplify (beaconData) { 36 | if (typeof beaconData === "object" && Array.isArray(beaconData)) { 37 | // beacon data is strange 38 | // it comes in an array of objects 39 | let newBeaconData = {}; 40 | for (const beaconDataItem of beaconData) { 41 | newBeaconData = Object.assign(newBeaconData, beaconDataItem); 42 | } 43 | return newBeaconData; 44 | } 45 | return beaconData; 46 | } 47 | 48 | static _handleLocalBeaconsList (pPanel, pLocalBeaconsListData) { 49 | const allBeacons = pLocalBeaconsListData.return[0]; 50 | for (const minionId in allBeacons) { 51 | const minionData = allBeacons[minionId]; 52 | for (const beaconName in minionData) { 53 | if (beaconName === "enabled") { 54 | // beacons flag 55 | if (minionData.enabled === false) { 56 | const tr = Issues.addIssue(pPanel, "disabled-beacons", minionId); 57 | Issues.addIssueMsg(tr, "Beacons on minion '" + minionId + "' are disabled"); 58 | Issues.addIssueCmd(tr, "Enable beacons", minionId, ["beacons.enable"]); 59 | Issues.addIssueNav(tr, "beacons-minion", {"minionid": minionId}); 60 | } 61 | } else { 62 | const beaconData = BeaconsIssues.simplify(minionData[beaconName]); 63 | if (beaconData.enabled === false) { 64 | const tr = Issues.addIssue(pPanel, "disabled-beacon", minionId + "-" + beaconName); 65 | Issues.addIssueMsg(tr, "Beacon '" + beaconName + "' on '" + minionId + "' is disabled"); 66 | Issues.addIssueCmd(tr, "Enable beacon", minionId, ["beacons.enable_beacon", beaconName]); 67 | Issues.addIssueCmd(tr, "Delete beacon", minionId, ["beacons.delete", beaconName]); 68 | Issues.addIssueNav(tr, "beacons-minion", {"minionid": minionId}); 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /saltgui/static/scripts/issues/Issues.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {DropDownMenu} from "../DropDown.js"; 4 | import {Utils} from "../Utils.js"; 5 | 6 | export class Issues { 7 | 8 | static removeCategory (pPanel, pCatName) { 9 | const rows = pPanel.table.tBodies[0].childNodes; 10 | for (const tr of rows) { 11 | if (tr.myCatName === pCatName) { 12 | tr.parentNode.removeChild(tr); 13 | } 14 | } 15 | } 16 | 17 | static removeIssue (pPanel, pCatName, pIssueName) { 18 | const rows = pPanel.table.tBodies[0].childNodes; 19 | for (const tr of rows) { 20 | if (tr.myCatName === pCatName && tr.myIssueName === pIssueName) { 21 | tr.parentNode.removeChild(tr); 22 | } 23 | } 24 | } 25 | 26 | static readyCategory (pPanel, pMsg) { 27 | 28 | // remove the "loading info..." message 29 | pPanel.msg2.removeChild(pMsg); 30 | 31 | pPanel.issuesStatus = Utils.txtZeroOneMany( 32 | pPanel.table.tBodies[0].children.length, 33 | "No issues", "{0} issue", "{0} issues"); 34 | pPanel.updateFooter(); 35 | 36 | // any category still loading? 37 | if (pPanel.msg2.childNodes.length > 0) { 38 | // not yet 39 | return; 40 | } 41 | 42 | pPanel.setTableSortable("Description", "asc"); 43 | } 44 | 45 | static addIssue (pPanel, pCatName, pIssueName) { 46 | 47 | // remove a previous incarnation of the same issue 48 | Issues.removeIssue(pPanel, pCatName, pIssueName); 49 | 50 | const theTr = Utils.createTr(); 51 | 52 | const menu = new DropDownMenu(theTr, "smaller"); 53 | theTr.menu = menu; 54 | 55 | const descTd = Utils.createTd(); 56 | const descSpan = Utils.createSpan("desc"); 57 | descTd.appendChild(descSpan); 58 | theTr.appendChild(descTd); 59 | 60 | theTr.myCatName = pCatName; 61 | theTr.myIssueName = pIssueName; 62 | theTr.panel = pPanel; 63 | 64 | pPanel.table.tBodies[0].appendChild(theTr); 65 | 66 | return theTr; 67 | } 68 | 69 | static addIssueMsg (pTr, pTitle) { 70 | const desc = pTr.querySelector("td .desc"); 71 | desc.innerText = pTitle; 72 | } 73 | 74 | static addIssueErr (pTr, pErrorMsg) { 75 | const desc = pTr.querySelector("td .desc"); 76 | Utils.addToolTip(desc, pErrorMsg); 77 | } 78 | 79 | static addIssueCmd (pTr, pTitle, pTarget, pCommand) { 80 | pTr.menu.addMenuItem(pTitle + "...", () => { 81 | pTr.panel.runCommand("", pTarget, pCommand); 82 | }); 83 | 84 | if (pTr.hasClick !== true) { 85 | pTr.addEventListener("click", (pClickEvent) => { 86 | pTr.panel.runCommand("", pTarget, pCommand); 87 | pClickEvent.stopPropagation(); 88 | }); 89 | } 90 | pTr.hasClick = true; 91 | } 92 | 93 | static addIssueNav (pTr, pPage, pArgs) { 94 | let title; 95 | if (pPage.endsWith("-minion")) { 96 | // when unclear, add "for this minion" to title 97 | title = "Go to " + pPage.replace("-minion", "") + " page"; 98 | } else { 99 | title = "Go to " + pPage + " page"; 100 | } 101 | pTr.menu.addMenuItem(title, (pClickEvent) => { 102 | pTr.panel.router.goTo(pPage, pArgs, undefined, pClickEvent); 103 | }); 104 | 105 | if (pTr.hasClick !== true) { 106 | pTr.addEventListener("click", (pClickEvent) => { 107 | pTr.panel.router.goTo(pPage, pArgs); 108 | pClickEvent.stopPropagation(); 109 | }); 110 | } 111 | pTr.hasClick = true; 112 | } 113 | 114 | onGetIssues (pPanel, pTitle) { 115 | this.api = pPanel.api; 116 | 117 | const msg = Utils.createDiv("msg", "(loading info for " + pTitle + ")"); 118 | pPanel.msg2.appendChild(msg); 119 | 120 | return msg; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /saltgui/static/scripts/issues/JobsRunning.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Issues} from "./Issues.js"; 4 | 5 | export class JobsRunningIssues extends Issues { 6 | 7 | onGetIssues (pPanel) { 8 | 9 | const msg = super.onGetIssues(pPanel, "JOBS-RUNNING"); 10 | 11 | const runnerJobsActivePromise = this.api.getRunnerJobsActive(); 12 | 13 | runnerJobsActivePromise.then((pRunnerJobsActiveData) => { 14 | Issues.removeCategory(pPanel, "active-jobs"); 15 | JobsRunningIssues._handleRunnerJobsActive(pPanel, pRunnerJobsActiveData); 16 | Issues.readyCategory(pPanel, msg); 17 | return true; 18 | }, (pRunnerJobsActiveMsg) => { 19 | Issues.removeCategory(pPanel, "active-jobs"); 20 | const tr = Issues.addIssue(pPanel, "active-jobs", "retrieving"); 21 | Issues.addIssueMsg(tr, "Could not retrieve list of jobs"); 22 | Issues.addIssueErr(tr, pRunnerJobsActiveMsg); 23 | Issues.readyCategory(pPanel, msg); 24 | return false; 25 | }); 26 | 27 | return runnerJobsActivePromise; 28 | } 29 | 30 | static _handleRunnerJobsActive (pPanel, pRunnerJobsActiveJobsData) { 31 | const allJobsDict = pRunnerJobsActiveJobsData.return[0]; 32 | const then = new Date(); 33 | // ignore jobs that were started less than 60 seconds ago 34 | // so that we do not detect our own jobs; and 35 | // so that we do not complain about trivial stuff 36 | then.setTime(then.getTime() - 60000); 37 | let thenStr = then. 38 | toISOString(). 39 | replace(/[-:.A-Z]/g, ""). 40 | substring(0, 20); 41 | while (thenStr.length < 20) { 42 | thenStr += "0"; 43 | } 44 | for (const jobId in allJobsDict) { 45 | if (jobId > thenStr) { 46 | continue; 47 | } 48 | const job = allJobsDict[jobId]; 49 | const tr = Issues.addIssue(pPanel, "active-jobs", jobId); 50 | Issues.addIssueMsg(tr, "Job '" + jobId + "' (" + job.Function + ") is still running"); 51 | Issues.addIssueNav(tr, "job", {"id": jobId}); 52 | Issues.addIssueCmd(tr, "Terminate job", "*", ["saltutil.term_job", jobId]); 53 | Issues.addIssueCmd(tr, "Kill job", "*", ["saltutil.kill_job", jobId]); 54 | Issues.addIssueCmd(tr, "Signal job", "*", ["saltutil.signal_job", jobId, "signal=", ""]); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /saltgui/static/scripts/issues/Keys.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Issues} from "./Issues.js"; 4 | 5 | export class KeysIssues extends Issues { 6 | 7 | onGetIssues (pPanel) { 8 | 9 | const msg = super.onGetIssues(pPanel, "KEYS"); 10 | 11 | const wheelKeyListAllPromise = this.api.getWheelKeyListAll(); 12 | 13 | wheelKeyListAllPromise.then((pWheelKeyListAllData) => { 14 | Issues.removeCategory(pPanel, "unaccepted-key"); 15 | KeysIssues._handleKeysWheelKeyListAll(pPanel, pWheelKeyListAllData); 16 | Issues.readyCategory(pPanel, msg); 17 | return true; 18 | }, (pWheelKeyListAllMsg) => { 19 | Issues.removeCategory(pPanel, "unaccepted-key"); 20 | const tr = Issues.addIssue(pPanel, "unaccepted-key", "retrieving"); 21 | Issues.addIssueMsg(tr, "Could not retrieve list of unaccepted keys"); 22 | Issues.addIssueErr(tr, pWheelKeyListAllMsg); 23 | Issues.readyCategory(pPanel, msg); 24 | return false; 25 | }); 26 | 27 | return wheelKeyListAllPromise; 28 | } 29 | 30 | static _handleKeysWheelKeyListAll (pPanel, pWheelKeyListAllData) { 31 | const allKeysDict = pWheelKeyListAllData.return[0].data.return; 32 | for (const minionId of allKeysDict.minions_pre) { 33 | // no direct commands 34 | // as multiple commands are applicable: accept, reject, delete 35 | // and for "accept", the fingerprint should be inspected first 36 | const tr = Issues.addIssue(pPanel, "unaccepted-key", minionId); 37 | Issues.addIssueMsg(tr, "Key for minion '" + minionId + "' is unaccepted"); 38 | Issues.addIssueNav(tr, "keys", {}); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /saltgui/static/scripts/issues/NotConnected.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Issues} from "./Issues.js"; 4 | 5 | export class NotConnectedIssues extends Issues { 6 | 7 | onGetIssues (pPanel) { 8 | 9 | const msg = super.onGetIssues(pPanel, "NOT-CONNECTED"); 10 | 11 | const wheelKeyListAllPromise = this.api.getWheelKeyListAll(); 12 | const wheelMinionsConnectedPromise = this.api.getWheelMinionsConnected(); 13 | 14 | wheelKeyListAllPromise.then((pWheelKeyListAllData) => { 15 | Issues.removeCategory(pPanel, "not-connected"); 16 | return pWheelKeyListAllData; 17 | }, (pWheelKeyListAllMsg) => { 18 | Issues.removeCategory(pPanel, "not-connected"); 19 | const tr = Issues.addIssue(pPanel, "not-connected", "retrieving-keys"); 20 | Issues.addIssueMsg(tr, "Could not retrieve list of keys"); 21 | Issues.addIssueErr(tr, pWheelKeyListAllMsg); 22 | return false; 23 | }); 24 | 25 | wheelMinionsConnectedPromise.then((pWheelMinionsConnectedData) => { 26 | Issues.removeCategory(pPanel, "not-connected"); 27 | return pWheelMinionsConnectedData; 28 | }, (pWheelMinionsConnectedMsg) => { 29 | Issues.removeCategory(pPanel, "not-connected"); 30 | const tr = Issues.addIssue(pPanel, "not-connected", "retrieving-connected"); 31 | Issues.addIssueMsg(tr, "Could not retrieve list of connected minions"); 32 | Issues.addIssueErr(tr, pWheelMinionsConnectedMsg); 33 | return false; 34 | }); 35 | 36 | /* eslint-disable compat/compat */ 37 | /* Promise.all() is not supported in op_mini all, IE 11 compat/compat */ 38 | const allPromise = Promise.all([wheelKeyListAllPromise, wheelMinionsConnectedPromise]); 39 | /* eslint-enable compat/compat */ 40 | allPromise.then((results) => { 41 | Issues.readyCategory(pPanel, msg); 42 | const wheelKeyListAllData = results[0]; 43 | const wheelMinionsConnectedData = results[1]; 44 | NotConnectedIssues._handleNotConnected(pPanel, wheelKeyListAllData, wheelMinionsConnectedData); 45 | }, (error) => { 46 | Issues.readyCategory(pPanel, msg); 47 | console.error(error); 48 | }); 49 | 50 | return allPromise; 51 | } 52 | 53 | static _handleNotConnected (pPanel, pWheelKeyListAllData, pWheelMinionsConnectedData) { 54 | const allMinions = pWheelKeyListAllData.return[0].data.return.minions; 55 | const allConnected = pWheelMinionsConnectedData.return[0].data.return; 56 | for (const minionId of allMinions) { 57 | if (allConnected.includes(minionId)) { 58 | continue; 59 | } 60 | // no direct commands, we don't know any useful ones 61 | const tr = Issues.addIssue(pPanel, "not-connected", minionId); 62 | Issues.addIssueMsg(tr, "Minion '" + minionId + "' is not connected"); 63 | Issues.addIssueNav(tr, "minions", {}); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /saltgui/static/scripts/issues/Schedules.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Issues} from "./Issues.js"; 4 | 5 | export class SchedulesIssues extends Issues { 6 | 7 | onGetIssues (pPanel) { 8 | 9 | const msg = super.onGetIssues(pPanel, "SCHEDULES"); 10 | 11 | const localScheduleListPromise = this.api.getLocalScheduleList(null); 12 | 13 | localScheduleListPromise.then((pLocalScheduleListData) => { 14 | Issues.removeCategory(pPanel, "disabled-schedulers"); 15 | Issues.removeCategory(pPanel, "disabled-schedules"); 16 | SchedulesIssues._handleLocalScheduleList(pPanel, pLocalScheduleListData); 17 | Issues.readyCategory(pPanel, msg); 18 | return true; 19 | }, (pLocalScheduleListMsg) => { 20 | Issues.removeCategory(pPanel, "disabled-schedulers"); 21 | const tr1 = Issues.addIssue(pPanel, "disabled-schedulers", "retrieving"); 22 | Issues.addIssueMsg(tr1, "Could not retrieve list of schedulers"); 23 | Issues.addIssueErr(tr1, pLocalScheduleListMsg); 24 | Issues.removeCategory(pPanel, "disabled-schedules"); 25 | const tr2 = Issues.addIssue(pPanel, "disabled-schedules", "retrieving"); 26 | Issues.addIssueMsg(tr2, "Could not retrieve list of schedules"); 27 | Issues.addIssueErr(tr2, pLocalScheduleListMsg); 28 | Issues.readyCategory(pPanel, msg); 29 | return false; 30 | }); 31 | 32 | return localScheduleListPromise; 33 | } 34 | 35 | static _handleLocalScheduleList (pPanel, pLocalScheduleListData) { 36 | 37 | const allSchedules = pLocalScheduleListData.return[0]; 38 | 39 | for (const minionId in allSchedules) { 40 | const minionData = allSchedules[minionId]; 41 | for (const key in minionData) { 42 | if (key === "enabled") { 43 | // scheduler flag 44 | if (minionData.enabled === false) { 45 | const tr = Issues.addIssue(pPanel, "disabled-schedulers", minionId); 46 | Issues.addIssueMsg(tr, "Scheduler on '" + minionId + "' is disabled"); 47 | Issues.addIssueCmd(tr, "Enable scheduler", minionId, ["schedule.enable"]); 48 | Issues.addIssueNav(tr, "schedules-minion", {"minionid": minionId}); 49 | } 50 | } else { 51 | const jobData = minionData[key]; 52 | if (jobData.enabled === false) { 53 | const tr = Issues.addIssue(pPanel, "disabled-schedules", minionId + "-" + key); 54 | Issues.addIssueMsg(tr, "Schedule '" + key + "' on '" + minionId + "' is disabled"); 55 | Issues.addIssueCmd(tr, "Enable schedule", minionId, ["schedule.enable_job", key]); 56 | Issues.addIssueCmd(tr, "Delete schedule", minionId, ["schedule.delete", key]); 57 | Issues.addIssueNav(tr, "schedules-minion", {"minionid": minionId}); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /saltgui/static/scripts/output/OutputHighstateSummaryOriginal.js: -------------------------------------------------------------------------------- 1 | import {Output} from "./Output.js"; 2 | import {Utils} from "../Utils.js"; 3 | 4 | export class OutputHighstateSummaryOriginal { 5 | 6 | static addPercentage (pCount, pTotal) { 7 | return (100 * pCount / pTotal).toLocaleString(undefined, {"maximumFractionDigits": 1, "minimumFractionDigits": 1}) + "%"; 8 | } 9 | 10 | static addSummarySpan (pDiv, pMinionId, pSucceeded, pFailed, pSkipped, pTotalMilliSeconds, pChangesSummary) { 11 | 12 | let txt = "\nSummary for " + pMinionId; 13 | txt += "\n------------"; 14 | const summarySpan = Utils.createSpan("", txt); 15 | summarySpan.style.color = "aqua"; 16 | pDiv.append(summarySpan); 17 | 18 | const total = pSucceeded + pSkipped + pFailed; 19 | 20 | txt = "\nSucceeded: " + pSucceeded; 21 | const succeededSpan = Utils.createSpan("task-success", txt); 22 | pDiv.append(succeededSpan); 23 | 24 | if (pChangesSummary > 0) { 25 | txt = " ("; 26 | const oSpan = Utils.createSpan("", txt); 27 | oSpan.style.color = "white"; 28 | pDiv.append(oSpan); 29 | 30 | txt = "changed=" + pChangesSummary; 31 | const changedSpan = Utils.createSpan("task-success-changes", txt); 32 | pDiv.append(changedSpan); 33 | 34 | txt = ")"; 35 | const cSpan = Utils.createSpan("", txt); 36 | cSpan.style.color = "white"; 37 | pDiv.append(cSpan); 38 | } 39 | 40 | txt = "\nFailed: " + pFailed; 41 | const failedSpan = Utils.createSpan("", txt); 42 | if (pFailed > 0) { 43 | failedSpan.classList.add("task-failure"); 44 | } else { 45 | failedSpan.style.color = "aqua"; 46 | } 47 | pDiv.append(failedSpan); 48 | 49 | const stateOutputPct = Utils.getStorageItemBoolean("session", "state_output_pct"); 50 | if (stateOutputPct) { 51 | txt = "\nSuccess %: " + OutputHighstateSummaryOriginal.addPercentage(pSucceeded, total); 52 | const successSpan = Utils.createSpan("task-success", txt); 53 | pDiv.append(successSpan); 54 | 55 | txt = "\nFailure %: " + OutputHighstateSummaryOriginal.addPercentage(pFailed, total); 56 | const failureSpan = Utils.createSpan("", txt); 57 | if (pFailed > 0) { 58 | failureSpan.classList.add("task-failure"); 59 | } else { 60 | failureSpan.style.color = "aqua"; 61 | } 62 | pDiv.append(failureSpan); 63 | } 64 | 65 | txt = "\n------------"; 66 | txt += "\nTotal states run: " + total; 67 | txt += "\nTotal run time: " + Output.getDuration(pTotalMilliSeconds); 68 | const totalsSpan = Utils.createSpan("", txt); 69 | totalsSpan.style.color = "aqua"; 70 | pDiv.append(totalsSpan); 71 | pDiv.style.cursor = "pointer"; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /saltgui/static/scripts/output/OutputHighstateSummarySaltGui.js: -------------------------------------------------------------------------------- 1 | import {Output} from "./Output.js"; 2 | import {Utils} from "../Utils.js"; 3 | 4 | export class OutputHighstateSummarySaltGui { 5 | 6 | static addPercentage (pCount, pTotal) { 7 | const stateOutputPct = Utils.getStorageItemBoolean("session", "state_output_pct"); 8 | 9 | if (!stateOutputPct) { 10 | return pCount; 11 | } 12 | 13 | return pCount + " (" + (100 * pCount / pTotal).toLocaleString(undefined, {"maximumFractionDigits": 1, "minimumFractionDigits": 1}) + "%)"; 14 | } 15 | 16 | static addSummarySpan (pDiv, pSucceeded, pFailed, pSkipped, pTotalMilliSeconds, pChangesDetail, pHidden) { 17 | 18 | // add a summary line 19 | let line = ""; 20 | 21 | const total = pSucceeded + pSkipped + pFailed; 22 | 23 | if (pSucceeded) { 24 | line += ", " + OutputHighstateSummarySaltGui.addPercentage(pSucceeded, total) + " succeeded"; 25 | } 26 | if (pSkipped) { 27 | line += ", " + OutputHighstateSummarySaltGui.addPercentage(pSkipped, total) + " skipped"; 28 | } 29 | if (pFailed) { 30 | line += ", " + OutputHighstateSummarySaltGui.addPercentage(pFailed, total) + " failed"; 31 | } 32 | if (pHidden) { 33 | line += ", " + OutputHighstateSummarySaltGui.addPercentage(pHidden, total) + " hidden"; 34 | } 35 | if (total !== pSucceeded && total !== pSkipped && total !== pFailed) { 36 | // not a trivial total 37 | line += ", " + total + " total"; 38 | } 39 | 40 | // note that the number of changes may be higher or lower 41 | // than the number of tasks. tasks may contribute multiple 42 | // changes, or tasks may have no changes. 43 | line += Utils.txtZeroOneMany(pChangesDetail, "", ", {0} change", ", {0} changes"); 44 | 45 | // multiple durations and significant? 46 | if (total > 1 && pTotalMilliSeconds >= 10) { 47 | line += ", " + Output.getDuration(pTotalMilliSeconds); 48 | } 49 | 50 | if (line) { 51 | const txtDiv = Utils.createDiv("", line.substring(2)); 52 | pDiv.append(txtDiv); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /saltgui/static/scripts/output/OutputHighstateTaskFull.js: -------------------------------------------------------------------------------- 1 | import {Output} from "./Output.js"; 2 | import {OutputNested} from "./OutputNested.js"; 3 | import {Utils} from "../Utils.js"; 4 | 5 | export class OutputHighstateTaskFull { 6 | 7 | static getStateOutput (pTask, pTaskId, pTaskName, pFunctionName) { 8 | 9 | let txt = "----------"; 10 | 11 | txt += "\n ID: " + pTaskId; 12 | 13 | txt += "\n Function: " + pFunctionName; 14 | 15 | if (pTaskId !== pTaskName) { 16 | txt += "\n Name: " + pTaskName; 17 | } 18 | 19 | txt += "\n Result: " + JSON.stringify(pTask.result); 20 | 21 | if (pTask.comment) { 22 | txt += "\n Comment: " + pTask.comment; 23 | } 24 | 25 | if (pTask.start_time) { 26 | // start_time is set by the original minion in its own timezone 27 | // we have no knowledge of that timezone, so no enhanced presentation here 28 | const startTime = Output.dateTimeStr("1999, Sep 9 " + pTask.start_time, null, null, true); 29 | txt += "\n Started: " + startTime; 30 | } 31 | 32 | if (pTask.duration >= 10) { 33 | txt += "\n Duration: " + Output.getDuration(pTask.duration); 34 | } 35 | 36 | txt += "\n Changes:"; 37 | 38 | let hasChanges = false; 39 | let chgs = null; 40 | if (pTask["changes"] !== undefined) { 41 | chgs = pTask.changes; 42 | const keys = Object.keys(chgs); 43 | if (keys.length === 2 && keys[0] === "out" && keys[1] === "ret") { 44 | chgs = chgs["ret"]; 45 | } 46 | const str = JSON.stringify(chgs); 47 | if (str !== "{}") { 48 | hasChanges = true; 49 | } 50 | } 51 | 52 | if (hasChanges) { 53 | txt += "\n" + OutputNested.formatNESTED(chgs, 14); 54 | } 55 | 56 | return Utils.createSpan("", txt); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /saltgui/static/scripts/output/OutputHighstateTaskTerse.js: -------------------------------------------------------------------------------- 1 | import {Output} from "./Output.js"; 2 | import {Utils} from "../Utils.js"; 3 | 4 | export class OutputHighstateTaskTerse { 5 | 6 | static getStateOutput (pTask, pTaskName, pFunctionName) { 7 | 8 | let txt = "Name: " + pTaskName; 9 | txt += " - Function: " + pFunctionName; 10 | txt += " - Result: "; 11 | if (JSON.stringify(pTask["changes"]) !== "{}") { 12 | txt += "Changed"; 13 | } else if (pTask["result"] === false) { 14 | txt += "Failed"; 15 | } else if (pTask["result"] === undefined) { 16 | txt += "Differs"; 17 | } else { 18 | txt += "Clean"; 19 | } 20 | if (pTask.start_time) { 21 | // start_time is set by the original minion in its own timezone 22 | // we have no knowledge of that timezone, so no enhanced presentation here 23 | const startTime = Output.dateTimeStr("1999, Sep 9 " + pTask.start_time, null, null, true); 24 | txt += " - Started: " + startTime; 25 | } 26 | if (pTask.duration >= 10) { 27 | txt += " - Duration: " + Output.getDuration(pTask.duration); 28 | } 29 | 30 | return Utils.createSpan("", txt); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /saltgui/static/scripts/output/OutputJson.js: -------------------------------------------------------------------------------- 1 | export class OutputJson { 2 | 3 | // format an object as JSON 4 | // returns NULL when it is not a simple object 5 | // i.e. no multi-line objects, no indentation here 6 | static _formatSimpleJSON (pValue) { 7 | 8 | if (pValue === null) { 9 | // null is an object, but not really 10 | // leave that to the builtin function 11 | return JSON.stringify(pValue); 12 | } 13 | 14 | if (pValue === undefined) { 15 | // JSON.stringify does not return a string for this 16 | // but again a value undefined, we need a string 17 | return "undefined"; 18 | } 19 | 20 | if (typeof pValue === "string") { 21 | // JSON.stringify does not handle this properly 22 | // as it may leave numeric-text unquoted 23 | return "\"" + pValue.replace(/["\\]/g, "\\$&") + "\""; 24 | } 25 | 26 | if (typeof pValue !== "object") { 27 | // any other simple type 28 | // leave that to the builtin function 29 | return JSON.stringify(pValue); 30 | } 31 | 32 | if (Array.isArray(pValue) && pValue.length === 0) { 33 | // show the brackets for an empty array a bit wider apart 34 | return "[ ]"; 35 | } 36 | 37 | if (Array.isArray(pValue) && pValue.length === 1 && typeof pValue[0] !== "object") { 38 | // show the brackets for a simple array a bit wider apart 39 | return "[ " + JSON.stringify(pValue[0]) + " ]"; 40 | } 41 | 42 | if (!Array.isArray(pValue) && Object.keys(pValue).length === 0) { 43 | // show the brackets for an empty object a bit wider apart 44 | return "{ }"; 45 | } 46 | 47 | // do not use Object.values as eslint does understand that because it is ES8/2017 48 | if (!Array.isArray(pValue) && Object.keys(pValue).length === 1 && typeof pValue[Object.keys(pValue)[0]] !== "object") { 49 | // show the brackets for a simple object a bit wider apart 50 | return "{ " + JSON.stringify(Object.keys(pValue)[0]) + ": " + JSON.stringify(pValue[Object.keys(pValue)[0]]) + " }"; 51 | } 52 | 53 | return null; 54 | } 55 | 56 | // format an object as JSON 57 | // based on an initial indentation and an indentation increment 58 | static formatJSON (pValue, pIndentLevel = 0) { 59 | 60 | // indent each level with 4 spaces 61 | const indentStep = 4; 62 | 63 | let str = OutputJson._formatSimpleJSON(pValue); 64 | if (str !== null) { 65 | return str; 66 | } 67 | 68 | if (Array.isArray(pValue)) { 69 | // an array 70 | // put each element on its own line 71 | str = "["; 72 | let aSeparator = ""; 73 | for (const elem of pValue) { 74 | str += aSeparator + "\n" + " ".repeat(pIndentLevel + indentStep) + 75 | OutputJson.formatJSON(elem, pIndentLevel + indentStep); 76 | aSeparator = ","; 77 | } 78 | str += "\n" + " ".repeat(pIndentLevel) + "]"; 79 | return str; 80 | } 81 | 82 | // regular object 83 | // put each name+value on its own line 84 | str = "{"; 85 | let oSeparator = ""; 86 | // do not use Object.entries, that is not supported by the test framework as it is ES8/2017 87 | const sortedKeys = Object.keys(pValue).sort((aa, bb) => aa.localeCompare(bb, "en", {"numeric": true})); 88 | for (const key of sortedKeys) { 89 | const item = pValue[key]; 90 | str += oSeparator + "\n" + " ".repeat(pIndentLevel + indentStep) + JSON.stringify(key) + ": " + 91 | OutputJson.formatJSON(item, pIndentLevel + indentStep); 92 | oSeparator = ","; 93 | } 94 | str += "\n" + " ".repeat(pIndentLevel) + "}"; 95 | return str; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /saltgui/static/scripts/output/OutputNested.js: -------------------------------------------------------------------------------- 1 | import {Character} from "../Character.js"; 2 | 3 | export class OutputNested { 4 | 5 | // heavily inspired by the implementation for NESTED output 6 | // as originally implemented in salt/output/nested.py from Salt 7 | 8 | static _ustring (pIndent, pTxt, pPrefix = "", pSuffix = "") { 9 | return " ".repeat(pIndent) + pPrefix + pTxt + pSuffix; 10 | } 11 | 12 | static display (pValue, pIndent, pPrefix, pOutArray) { 13 | if (pValue === null) { 14 | pOutArray.push(OutputNested._ustring(pIndent, "None", pPrefix)); 15 | } else if (pValue === undefined) { 16 | pOutArray.push(OutputNested._ustring(pIndent, "undefined", pPrefix)); 17 | } else if (typeof pValue === "boolean" || typeof pValue === "number") { 18 | pOutArray.push(OutputNested._ustring(pIndent, pValue, pPrefix)); 19 | } else if (typeof pValue === "string") { 20 | let isFirstLine = true; 21 | pValue = pValue.replace(/\n$/, ""); 22 | for (const line of pValue.split("\n")) { 23 | let linePrefix = pPrefix; 24 | if (!isFirstLine) { 25 | linePrefix = " ".repeat(pPrefix.length); 26 | } 27 | pOutArray.push(OutputNested._ustring(pIndent, line, linePrefix)); 28 | isFirstLine = false; 29 | } 30 | } else if (typeof pValue === "object" && Array.isArray(pValue)) { 31 | for (const ind of pValue) { 32 | if (typeof ind === "object") { 33 | // including array 34 | pOutArray.push(OutputNested._ustring(pIndent, "|_")); 35 | let prefix; 36 | if (Array.isArray(ind)) { 37 | prefix = "-" + Character.NO_BREAK_SPACE; 38 | } else { 39 | prefix = ""; 40 | } 41 | OutputNested.display(ind, pIndent + 2, prefix, pOutArray); 42 | } else { 43 | OutputNested.display(ind, pIndent, "-" + Character.NO_BREAK_SPACE, pOutArray); 44 | } 45 | } 46 | } else if (typeof pValue === "object") { 47 | if (pIndent) { 48 | pOutArray.push(OutputNested._ustring(pIndent, "----------")); 49 | } 50 | const sortedKeys = Object.keys(pValue).sort((aa, bb) => aa.localeCompare(bb, "en", {"numeric": true})); 51 | for (const key of sortedKeys) { 52 | const val = pValue[key]; 53 | pOutArray.push(OutputNested._ustring(pIndent, key, pPrefix, ":")); 54 | if (val === null) { 55 | // VOID 56 | } else if (val === "") { 57 | // VOID 58 | } else { 59 | OutputNested.display(val, pIndent + 4, "", pOutArray); 60 | } 61 | } 62 | } 63 | return pOutArray; 64 | } 65 | 66 | static formatNESTED (pValue, pIndentLevel = 0) { 67 | const lines = OutputNested.display(pValue, pIndentLevel, "", []); 68 | return lines.join("\n"); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /saltgui/static/scripts/output/OutputYaml.js: -------------------------------------------------------------------------------- 1 | import {Character} from "../Character.js"; 2 | 3 | export class OutputYaml { 4 | 5 | // format an object as YAML 6 | // returns NULL when it is not a simple object 7 | // i.e. no multi-line objects, no indentation here 8 | static _formatSimpleYAML (pValue) { 9 | 10 | if (pValue === null) { 11 | return "null"; 12 | } 13 | 14 | if (pValue === undefined) { 15 | return "undefined"; 16 | } 17 | 18 | if (typeof pValue === "boolean") { 19 | return pValue ? "true" : "false"; 20 | } 21 | 22 | if (typeof pValue === "string") { 23 | let needQuotes = false; 24 | 25 | // simple number with extra 0's at the start is still a string 26 | if (pValue.match(/^0[0-9]+$/)) { 27 | needQuotes = true; 28 | } 29 | 30 | if (!isNaN(Number(pValue))) { 31 | needQuotes = true; 32 | } 33 | 34 | if (pValue.match(/^$/)) { 35 | needQuotes = true; 36 | } 37 | 38 | if (pValue.match(/^ /)) { 39 | needQuotes = true; 40 | } 41 | if (pValue.match(/ $/)) { 42 | needQuotes = true; 43 | } 44 | 45 | if (pValue.match(/^@/)) { 46 | needQuotes = true; 47 | } 48 | if (pValue.match(/^`/)) { 49 | needQuotes = true; 50 | } 51 | if (pValue.match(/'/)) { 52 | needQuotes = true; 53 | } 54 | if (pValue.match(/^%/)) { 55 | needQuotes = true; 56 | } 57 | 58 | if (!pValue.match(/^[-a-z0-9_()./:+ ]+$/i)) { 59 | needQuotes = true; 60 | } 61 | 62 | if (!needQuotes) { 63 | return pValue; 64 | } 65 | return "'" + pValue.replace(/['\\]/g, "\\$&") + "'"; 66 | } 67 | 68 | if (typeof pValue !== "object") { 69 | return String(pValue); 70 | } 71 | 72 | if (Array.isArray(pValue) && pValue.length === 0) { 73 | // show the brackets for an empty array a bit wider apart 74 | return "[ ]"; 75 | } 76 | 77 | if (!Array.isArray(pValue) && Object.keys(pValue).length === 0) { 78 | // show the brackets for an empty object a bit wider apart 79 | return "{ }"; 80 | } 81 | 82 | return null; 83 | } 84 | 85 | // format an object as YAML 86 | // based on an initial indentation and an indentation increment 87 | static formatYAML (pValue, pIndentLevel = 0) { 88 | 89 | // indent each level with this number of spaces 90 | // note that list items are indented with 2 spaces 91 | // independently of this setting to match the prefix "- " 92 | const indentStep = 2; 93 | 94 | const str = OutputYaml._formatSimpleYAML(pValue); 95 | if (str !== null) { 96 | return str; 97 | } 98 | 99 | if (Array.isArray(pValue)) { 100 | let aOut = ""; 101 | let aSeparator = ""; 102 | for (const item of pValue) { 103 | aOut += aSeparator + "-" + Character.NO_BREAK_SPACE + OutputYaml.formatYAML(item, pIndentLevel + 2); 104 | aSeparator = "\n" + " ".repeat(pIndentLevel); 105 | } 106 | return aOut; 107 | } 108 | 109 | // regular object 110 | let oOut = ""; 111 | let oSeparator = ""; 112 | const sortedKeys = Object.keys(pValue).sort((aa, bb) => aa.localeCompare(bb, "en", {"numeric": true})); 113 | for (const key of sortedKeys) { 114 | const item = pValue[key]; 115 | oOut += oSeparator + key + ":"; 116 | const systr = OutputYaml._formatSimpleYAML(item); 117 | if (systr !== null) { 118 | oOut += " " + systr; 119 | } else if (Array.isArray(item)) { 120 | oOut += "\n" + " ".repeat(pIndentLevel) + OutputYaml.formatYAML(item, pIndentLevel); 121 | } else if (typeof item === "object") { 122 | oOut += "\n" + " ".repeat(pIndentLevel + indentStep) + OutputYaml.formatYAML(item, pIndentLevel + indentStep); 123 | } else { 124 | /* istanbul ignore next */ 125 | oOut += "x" + OutputYaml.formatYAML(item, pIndentLevel + indentStep); 126 | } 127 | oSeparator = "\n" + " ".repeat(pIndentLevel); 128 | } 129 | return oOut; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Beacons.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {BeaconsPanel} from "../panels/Beacons.js"; 4 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class BeaconsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("beacons", "Beacons", "page-beacons", "button-beacons", pRouter); 12 | 13 | this.beacons = new BeaconsPanel(); 14 | super.addPanel(this.beacons); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/BeaconsMinion.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {BeaconsMinionPanel} from "../panels/BeaconsMinion.js"; 4 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class BeaconsMinionPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("beacons-minion", "Beacons", "page-beacons-minion", "button-beacons", pRouter); 12 | 13 | this.beaconsminion = new BeaconsMinionPanel(); 14 | super.addPanel(this.beaconsminion); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltBeaconEvent (pTag, pData) { 22 | this.beaconsminion.handleSaltBeaconEvent(pTag, pData); 23 | } 24 | 25 | handleSaltJobRetEvent (pData) { 26 | if (this.jobs) { 27 | this.jobs.handleSaltJobRetEvent(pData); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Events.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {EventsPanel} from "../panels/Events.js"; 4 | import {Page} from "./Page.js"; 5 | 6 | export class EventsPage extends Page { 7 | 8 | constructor (pRouter) { 9 | // don't use /events for the page, that url is reserved 10 | super("events", "Events", "page-events", "button-events", pRouter); 11 | 12 | this.events = new EventsPanel(); 13 | super.addPanel(this.events); 14 | } 15 | 16 | handleAnyEvent (tag, data) { 17 | this.events.handleAnyEvent(tag, data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Grains.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {GrainsPanel} from "../panels/Grains.js"; 4 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class GrainsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("grains", "Grains", "page-grains", "button-grains", pRouter); 12 | 13 | this.grains = new GrainsPanel(); 14 | super.addPanel(this.grains); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/GrainsMinion.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {GrainsMinionPanel} from "../panels/GrainsMinion.js"; 4 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class GrainsMinionPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("grains-minion", "Grains", "page-grains-minion", "button-grains", pRouter); 12 | 13 | this.grainsminion = new GrainsMinionPanel(); 14 | super.addPanel(this.grainsminion); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/HighState.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {HighStatePanel} from "../panels/HighState.js"; 4 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class HighStatePage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("highstate", "HighState", "page-highstate", "button-highstate", pRouter); 12 | 13 | this.highstate = new HighStatePanel(); 14 | super.addPanel(this.highstate); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | 27 | onHide () { 28 | this.highstate.onHide(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Issues.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {IssuesPanel} from "../panels/Issues.js"; 4 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class IssuesPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("issues", "issues", "page-issues", "button-issues", pRouter); 12 | 13 | this.issues = new IssuesPanel(); 14 | super.addPanel(this.issues); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | 27 | onHide () { 28 | this.issues.onHide(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Job.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobPanel} from "../panels/Job.js"; 4 | import {Page} from "./Page.js"; 5 | 6 | export class JobPage extends Page { 7 | 8 | constructor (pRouter) { 9 | super("job", "Job", "page-job", "button-jobs", pRouter); 10 | 11 | this.job = new JobPanel(); 12 | super.addPanel(this.job); 13 | } 14 | 15 | handleSaltJobRetEvent (pData) { 16 | this.job.handleSaltJobRetEvent(pData); 17 | } 18 | 19 | onHide () { 20 | this.job.onHide(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Jobs.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsDetailsPanel} from "../panels/JobsDetails.js"; 4 | import {Page} from "./Page.js"; 5 | 6 | export class JobsPage extends Page { 7 | 8 | constructor (pRouter) { 9 | super("jobs", "Jobs", "page-jobs", "button-jobs", pRouter); 10 | 11 | this.jobs = new JobsDetailsPanel(); 12 | super.addPanel(this.jobs); 13 | } 14 | 15 | handleSaltJobRetEvent (pData) { 16 | this.jobs.handleSaltJobRetEvent(pData); 17 | } 18 | 19 | onHide () { 20 | this.jobs.onHide(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Keys.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {KeysPanel} from "../panels/Keys.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class KeysPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("keys", "Keys", "page-keys", "button-keys", pRouter); 12 | 13 | this.keys = new KeysPanel(); 14 | super.addPanel(this.keys); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltAuthEvent (pData) { 22 | this.keys.handleSaltAuthEvent(pData); 23 | } 24 | 25 | handleSaltKeyEvent (pData) { 26 | this.keys.handleSaltKeyEvent(pData); 27 | } 28 | 29 | handleSaltJobRetEvent (pData) { 30 | if (this.jobs) { 31 | this.jobs.handleSaltJobRetEvent(pData); 32 | } 33 | } 34 | 35 | handleSyndicEvent () { 36 | this.keys.handleSyndicEvent(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Login.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {LoginPanel} from "../panels/Login.js"; 4 | import {Page} from "./Page.js"; 5 | 6 | export class LoginPage extends Page { 7 | 8 | constructor (pRouter) { 9 | super("login", "Login", "page-login", "", pRouter); 10 | 11 | this.login = new LoginPanel(); 12 | this.login.router = pRouter; 13 | super.addPanel(this.login); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Logout.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Page} from "./Page.js"; 4 | import {Utils} from "../Utils.js"; 5 | 6 | export class LogoutPage extends Page { 7 | 8 | constructor (pRouter) { 9 | super("logout", "Logout", "page-logout", "", pRouter); 10 | } 11 | 12 | onRegister () { 13 | // don't verify for invalid sessions too often 14 | // this happens only when the server was reset 15 | this.logoutInterval = window.setInterval(() => { 16 | this._logoutTimer(); 17 | }, 60000); 18 | 19 | // verify often for an expired session that we expect 20 | window.setInterval(() => { 21 | this._updateSessionTimeoutWarning(); 22 | }, 1000); 23 | } 24 | 25 | onShow () { 26 | // on touchscreens the menus do not go away by themselves 27 | Utils.hideAllMenus(true); 28 | 29 | this.api.logout().then(() => { 30 | this.router.goTo("login", {"reason": "logout"}); 31 | }); 32 | } 33 | 34 | _logoutTimer () { 35 | // are we logged in? 36 | const token = Utils.getStorageItem("session", "token"); 37 | if (!token) { 38 | return; 39 | } 40 | 41 | // just a random lightweight api call 42 | // that is not bound by the api permissions 43 | // very old versions of /stats did not properly 44 | // detect invalid sessions, but that was fixed 45 | const statsPromise = this.api.getStats(); 46 | // don't act in the callbacks 47 | // Api.apiRequest will do all the work 48 | statsPromise.then(() => true, (pHttpResponse) => { 49 | if (pHttpResponse.status === 500) { 50 | // assume this error applies only to /stats and 51 | // not to any regular api functions 52 | // may happen due to https://github.com/saltstack/salt/issues/59620 53 | // repeating this is not so useful 54 | if (this.logoutInterval) { 55 | clearInterval(this.logoutInterval); 56 | this.logoutInterval = null; 57 | } 58 | return; 59 | } 60 | this.api.logout().then(() => { 61 | this.router.goTo("login", {"reason": "session-cancelled"}); 62 | return false; 63 | }); 64 | }); 65 | } 66 | 67 | _updateSessionTimeoutWarning () { 68 | const warning = document.getElementById("warning"); 69 | 70 | if (Utils.getStorageItem("session", "token", null) === null) { 71 | // cannot force logout without session 72 | warning.style.display = ""; 73 | warning.innerText = "No session"; 74 | return; 75 | } 76 | 77 | const loginResponse = Utils.getStorageItemObject("session", "login_response"); 78 | 79 | const expireValue = loginResponse.expire; 80 | if (!expireValue) { 81 | warning.style.display = "none"; 82 | return; 83 | } 84 | 85 | const leftMillis = expireValue * 1000 - Date.now(); 86 | 87 | if (leftMillis <= 0) { 88 | warning.style.display = ""; 89 | warning.innerText = "Logout"; 90 | // logout, and redirect to login screen 91 | this.api.logout().then(() => { 92 | this.router.goTo("login", {"reason": "session-expired"}); 93 | return true; 94 | }, () => { 95 | this.router.goTo("login", {"reason": "session-expired"}); 96 | return false; 97 | }); 98 | return; 99 | } 100 | 101 | if (leftMillis > 60000) { 102 | // warn only in the last minute 103 | warning.style.display = "none"; 104 | warning.innerText = ""; 105 | return; 106 | } 107 | 108 | warning.style.display = ""; 109 | const left = new Date(leftMillis).toISOString(); 110 | if (left.startsWith("1970-01-01T")) { 111 | // remove the date prefix and the millisecond suffix 112 | warning.innerText = "Session expires in " + left.substring(11, 19); 113 | } else { 114 | // stupid fallback 115 | warning.innerText = "Session expires in " + leftMillis + " milliseconds"; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Minions.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {MinionsPanel} from "../panels/Minions.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class MinionsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("minions", "Minions", "page-minions", "button-minions", pRouter); 12 | 13 | this.minions = new MinionsPanel(); 14 | super.addPanel(this.minions); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Nodegroups.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {NodegroupsPanel} from "../panels/Nodegroups.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class NodegroupsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("nodegroups", "Nodegroups", "page-nodegroups", "button-nodegroups", pRouter); 12 | 13 | this.nodegroups = new NodegroupsPanel(); 14 | super.addPanel(this.nodegroups); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | 27 | /* eslint-disable class-methods-use-this */ 28 | isVisible () { 29 | /* eslint-enable class-methods-use-this */ 30 | // show nodegroups menu item if nodegroups defined 31 | const nodegroups = Utils.getStorageItemObject("session", "nodegroups"); 32 | return Object.keys(nodegroups).length > 0; 33 | } 34 | 35 | onHide () { 36 | this.nodegroups.onHide(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Options.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {OptionsPanel} from "../panels/Options.js"; 4 | import {Page} from "./Page.js"; 5 | import {StatsPanel} from "../panels/Stats.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class OptionsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("options", "Options", "page-options", "", pRouter); 12 | 13 | this.options = new OptionsPanel(); 14 | super.addPanel(this.options); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.stats = new StatsPanel(); 17 | super.addPanel(this.stats); 18 | } 19 | } 20 | 21 | onHide () { 22 | this.options.onHide(); 23 | if (this.stats) { 24 | this.stats.onHide(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Orchestrations.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {OrchestrationsPanel} from "../panels/Orchestrations.js"; 5 | import {Page} from "./Page.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class OrchestrationsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("orchestrations", "Orchestrations", "page-orchestrations", "button-orchestrations", pRouter); 12 | 13 | this.orchestrations = new OrchestrationsPanel(); 14 | super.addPanel(this.orchestrations); 15 | this.jobs = new JobsSummaryPanel(); 16 | super.addPanel(this.jobs); 17 | } 18 | 19 | /* eslint-disable class-methods-use-this */ 20 | isVisible () { 21 | /* eslint-enable class-methods-use-this */ 22 | // show orchestrations menu item if orchestrations defined 23 | return Utils.getStorageItemBoolean("session", "orchestrations"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Page.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {CommandBox} from "../CommandBox.js"; 4 | import {Router} from "../Router.js"; 5 | import {Utils} from "../Utils.js"; 6 | 7 | export class Page { 8 | 9 | constructor (pPath, pPageName, pPageSelector, pMenuItemSelector, pRouter) { 10 | this.path = pPath; 11 | this.name = pPageName; 12 | 13 | //
14 | //
15 | //
16 | let div = document.getElementById(pPageSelector); 17 | if (div === null) { 18 | const route = Utils.createDiv("route", "", pPageSelector); 19 | const dashboard = Utils.createDiv("dashboard"); 20 | route.append(dashboard); 21 | const routeContainer = document.getElementById("route-container"); 22 | routeContainer.append(route); 23 | div = route; 24 | } 25 | this.pageElement = div; 26 | this.router = pRouter; 27 | if (pMenuItemSelector) { 28 | this.menuItemElement1 = pMenuItemSelector + "1"; 29 | this.menuItemElement2 = pMenuItemSelector + "2"; 30 | } 31 | 32 | this.panels = []; 33 | this.api = pRouter.api; 34 | 35 | if (Utils.getQueryParam("popup") === "true") { 36 | const fullmenu = document.querySelector(".fullmenu"); 37 | fullmenu.style.display = "none"; 38 | const minimenu = document.querySelector(".minimenu"); 39 | minimenu.style.display = "none"; 40 | } 41 | 42 | const body = document.querySelector("body"); 43 | body.onkeyup = (keyUpEvent) => { 44 | if (!Utils.isValidKeyUpEvent(keyUpEvent)) { 45 | return; 46 | } 47 | 48 | if (this._handleTemplateKey(keyUpEvent)) { 49 | keyUpEvent.stopPropagation(); 50 | return; 51 | } 52 | 53 | if (this._handleMenuKey(keyUpEvent)) { 54 | keyUpEvent.stopPropagation(); 55 | // return; 56 | } 57 | }; 58 | } 59 | 60 | _handleTemplateKey (keyUpEvent) { 61 | const templateName = Utils.getStorageItem("session", "template_" + keyUpEvent.key, ""); 62 | if (templateName === "") { 63 | // key not bound to a template 64 | return false; 65 | } 66 | 67 | // apply template 68 | CommandBox.applyTemplateByName(templateName); 69 | CommandBox.showManualRun(this.api); 70 | return true; 71 | } 72 | 73 | _handleMenuKey (keyUpEvent) { 74 | if (keyUpEvent.key === "c") { 75 | CommandBox.showManualRun(this.api); 76 | return true; 77 | } 78 | 79 | const pages = Router._getPagesList(); 80 | const page = Utils.getStorageItem("session", "menu_" + keyUpEvent.key, ""); 81 | // Arrays.includes() is only available from ES7/2016 82 | if (page && (pages.length === 0 || pages.indexOf(page) >= 0)) { 83 | this.router.goTo(page); 84 | return true; 85 | } 86 | 87 | return false; 88 | } 89 | 90 | addPanel (pPanel) { 91 | pPanel.route = this; 92 | pPanel.router = this.router; 93 | const dashboard = this.pageElement.querySelector(".dashboard"); 94 | dashboard.append(pPanel.div); 95 | pPanel.api = this.api; 96 | if (this.panels.length > 0) { 97 | // hide all but the leftmost (=main) panel when printing 98 | pPanel.div.classList.add("no-print"); 99 | } 100 | this.panels.push(pPanel); 101 | } 102 | 103 | /* eslint-disable class-methods-use-this */ 104 | isVisible () { 105 | /* eslint-enable class-methods-use-this */ 106 | // a page is visible, unless the page decides otherwise 107 | return true; 108 | } 109 | 110 | static _updateMotd () { 111 | 112 | const motd = document.getElementById("motd"); 113 | 114 | const motdStatus = Utils.getStorageItem("session", "motd-status", ""); 115 | if (motdStatus === "hidden") { 116 | motd.style.display = "none"; 117 | return; 118 | } 119 | 120 | const saltMotdTxt = Utils.getStorageItem("session", "motd_txt", ""); 121 | const motdTxtDiv = document.getElementById("motdtxt"); 122 | motdTxtDiv.innerText = saltMotdTxt; 123 | motdTxtDiv.style.display = saltMotdTxt ? "" : "none"; 124 | 125 | const saltMotdHtml = Utils.getStorageItem("session", "motd_html", ""); 126 | const motdHtmlDiv = document.getElementById("motdhtml"); 127 | motdHtmlDiv.innerHTML = saltMotdHtml; 128 | motdHtmlDiv.style.display = saltMotdHtml ? "" : "none"; 129 | 130 | const motdCLoseButton1 = document.getElementById("close-motd"); 131 | // remove all event-handlers (yes, this is otherwise a silly assignment) 132 | // that makes it a different object! 133 | /* eslint-disable no-self-assign */ 134 | motdCLoseButton1.outerHTML = motdCLoseButton1.outerHTML; 135 | /* eslint-enable no-self-assign */ 136 | 137 | if (!saltMotdTxt && !saltMotdHtml) { 138 | // nothing to see, don't bother 139 | motd.style.display = "none"; 140 | return; 141 | } 142 | 143 | motd.style.display = ""; 144 | 145 | const motdCLoseButton2 = document.getElementById("close-motd"); 146 | // add the event-handler 147 | motdCLoseButton2.addEventListener("click", (pClickEvent) => { 148 | Utils.setStorageItem("session", "motd_txt", ""); 149 | Utils.setStorageItem("session", "motd_html", ""); 150 | motd.style.display = "none"; 151 | pClickEvent.stopPropagation(); 152 | }); 153 | } 154 | 155 | onShow () { 156 | for (const panel of this.panels) { 157 | panel.onShow(); 158 | } 159 | Page._updateMotd(); 160 | } 161 | 162 | clearPage () { 163 | for (const panel of this.panels) { 164 | panel.clearPanel(); 165 | } 166 | } 167 | 168 | refreshPage () { 169 | for (const panel of this.panels) { 170 | if (!panel.needsRefresh) { 171 | continue; 172 | } 173 | panel.needsRefresh = false; 174 | panel.clearPanel(); 175 | panel.onShow(); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Pillars.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {Page} from "./Page.js"; 5 | import {PillarsPanel} from "../panels/Pillars.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class PillarsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("pillars", "Pillars", "page-pillars", "button-pillars", pRouter); 12 | 13 | this.pillars = new PillarsPanel(); 14 | super.addPanel(this.pillars); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/PillarsMinion.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {Page} from "./Page.js"; 5 | import {PillarsMinionPanel} from "../panels/PillarsMinion.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class PillarsMinionPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("pillars-minion", "Pillars", "page-pillars-minion", "button-pillars", pRouter); 12 | 13 | this.pillarsminion = new PillarsMinionPanel(); 14 | super.addPanel(this.pillarsminion); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Reactors.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {Page} from "./Page.js"; 5 | import {ReactorsPanel} from "../panels/Reactors.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class ReactorsPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("reactors", "Reactors", "page-reactors", "button-reactors", pRouter); 12 | 13 | this.reactors = new ReactorsPanel(); 14 | super.addPanel(this.reactors); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | 27 | /* eslint-disable class-methods-use-this */ 28 | isVisible () { 29 | /* eslint-enable class-methods-use-this */ 30 | // show reactor menu item if reactors defined 31 | const reactors = Utils.getStorageItemList("session", "reactors"); 32 | return reactors.length > 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Schedules.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {Page} from "./Page.js"; 5 | import {SchedulesPanel} from "../panels/Schedules.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class SchedulesPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("schedules", "Schedules", "page-schedules", "button-schedules", pRouter); 12 | 13 | this.schedules = new SchedulesPanel(); 14 | super.addPanel(this.schedules); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/SchedulesMinion.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {Page} from "./Page.js"; 5 | import {SchedulesMinionPanel} from "../panels/SchedulesMinion.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class SchedulesMinionPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("schedules-minion", "Schedules", "page-schedules-minion", "button-schedules", pRouter); 12 | 13 | this.schedulesminion = new SchedulesMinionPanel(); 14 | super.addPanel(this.schedulesminion); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /saltgui/static/scripts/pages/Templates.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {JobsSummaryPanel} from "../panels/JobsSummary.js"; 4 | import {Page} from "./Page.js"; 5 | import {TemplatesPanel} from "../panels/Templates.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class TemplatesPage extends Page { 9 | 10 | constructor (pRouter) { 11 | super("templates", "Templates", "page-templates", "button-templates", pRouter); 12 | 13 | this.templates = new TemplatesPanel(); 14 | super.addPanel(this.templates); 15 | if (Utils.getQueryParam("popup") !== "true") { 16 | this.jobs = new JobsSummaryPanel(); 17 | super.addPanel(this.jobs); 18 | } 19 | } 20 | 21 | handleSaltJobRetEvent (pData) { 22 | if (this.jobs) { 23 | this.jobs.handleSaltJobRetEvent(pData); 24 | } 25 | } 26 | 27 | /* eslint-disable class-methods-use-this */ 28 | isVisible () { 29 | /* eslint-enable class-methods-use-this */ 30 | // show template menu item if templates defined 31 | const templates = Utils.getStorageItemObject("session", "templates"); 32 | return Object.keys(templates).length > 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/Beacons.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Panel} from "./Panel.js"; 4 | import {Utils} from "../Utils.js"; 5 | 6 | export class BeaconsPanel extends Panel { 7 | 8 | constructor () { 9 | super("beacons"); 10 | 11 | this.addTitle("Beacons"); 12 | this.addSearchButton(); 13 | this.addTable(["-menu-", "Minion", "Status", "Beacons"]); 14 | this.setTableSortable("Minion", "asc"); 15 | this.setTableClickable("page"); 16 | this.addMsg(); 17 | } 18 | 19 | onShow () { 20 | const wheelKeyListAllPromise = this.api.getWheelKeyListAll(); 21 | const localBeaconsListPromise = this.api.getLocalBeaconsList(null); 22 | 23 | this.nrMinions = 0; 24 | 25 | wheelKeyListAllPromise.then((pWheelKeyListAllData) => { 26 | this._handleBeaconsWheelKeyListAll(pWheelKeyListAllData); 27 | localBeaconsListPromise.then((pLocalBeaconsListData) => { 28 | this.updateMinions(pLocalBeaconsListData); 29 | return true; 30 | }, (pLocalBeaconsListMsg) => { 31 | const allMinionsErr = Utils.msgPerMinion(pWheelKeyListAllData.return[0].data.return.minions, JSON.stringify(pLocalBeaconsListMsg)); 32 | this.updateMinions({"return": [allMinionsErr]}); 33 | return false; 34 | }); 35 | return true; 36 | }, (pWheelKeyListAllMsg) => { 37 | this._handleBeaconsWheelKeyListAll(JSON.stringify(pWheelKeyListAllMsg)); 38 | Utils.ignorePromise(localBeaconsListPromise); 39 | return false; 40 | }); 41 | } 42 | 43 | static fixBeaconsMinion (pData) { 44 | if (typeof pData !== "object") { 45 | return pData; 46 | } 47 | 48 | // the data is an array of objects 49 | // where each object has one key 50 | // re-create as a normal object 51 | 52 | const ret = {"beacons": {}, "enabled": true}; 53 | 54 | for (const beaconName in pData) { 55 | // correct for empty list that returns this dummy value 56 | if (beaconName === "beacons" && JSON.stringify(pData[beaconName]) === "{}") { 57 | continue; 58 | } 59 | 60 | // "enabled" is always a boolean (when present) 61 | if (beaconName === "enabled") { 62 | ret.enabled = pData.enabled; 63 | continue; 64 | } 65 | 66 | // make one object from the settings 67 | // eliminates one layer in the datamodel 68 | // and looks much better 69 | const newData = {}; 70 | for (const elem of pData[beaconName]) { 71 | for (const valueKey in elem) { 72 | newData[valueKey] = elem[valueKey]; 73 | } 74 | } 75 | ret.beacons[beaconName] = newData; 76 | } 77 | 78 | return ret; 79 | } 80 | 81 | _handleBeaconsWheelKeyListAll (pWheelKeyListAllData) { 82 | if (this.showErrorRowInstead(pWheelKeyListAllData)) { 83 | return; 84 | } 85 | 86 | const keys = pWheelKeyListAllData.return[0].data.return; 87 | this.nrMinions = keys.minions.length; 88 | this.nrUnaccepted = keys.minions_pre.length; 89 | 90 | const minionIds = keys.minions.sort(); 91 | for (const minionId of minionIds) { 92 | const minionTr = this.addMinion(minionId); 93 | 94 | // preliminary dropdown menu 95 | this._addMenuItemShowBeacons(minionTr.dropdownmenu, minionId); 96 | 97 | minionTr.addEventListener("click", (pClickEvent) => { 98 | this.router.goTo("beacons-minion", {"minionid": minionId}, undefined, pClickEvent); 99 | pClickEvent.stopPropagation(); 100 | }); 101 | } 102 | 103 | this.updateFooter(); 104 | } 105 | 106 | updateOfflineMinion (pMinionId, pMinionsDict) { 107 | super.updateOfflineMinion(pMinionId, pMinionsDict); 108 | 109 | const minionTr = this.table.querySelector("#" + Utils.getIdFromMinionId(pMinionId)); 110 | 111 | // force same columns on all rows 112 | minionTr.appendChild(Utils.createTd("beaconinfo")); 113 | } 114 | 115 | updateMinion (pMinionData, pMinionId, pAllMinionsGrains) { 116 | 117 | pMinionData = BeaconsPanel.fixBeaconsMinion(pMinionData); 118 | 119 | super.updateMinion(null, pMinionId, pAllMinionsGrains); 120 | 121 | const minionTr = this.table.querySelector("#" + Utils.getIdFromMinionId(pMinionId)); 122 | 123 | if (typeof pMinionData === "object") { 124 | const cnt = Object.keys(pMinionData.beacons).length; 125 | let beaconInfoText = Utils.txtZeroOneMany(cnt, 126 | "no beacons", "{0} beacon", "{0} beacons"); 127 | if (!pMinionData.enabled) { 128 | beaconInfoText += " (disabled)"; 129 | } 130 | const beaconInfoTd = Utils.createTd("beaconinfo", beaconInfoText); 131 | beaconInfoTd.setAttribute("sorttable_customkey", cnt); 132 | minionTr.appendChild(beaconInfoTd); 133 | } else { 134 | const beaconInfoTd = Utils.createTd(); 135 | Utils.addErrorToTableCell(beaconInfoTd, pMinionData); 136 | minionTr.appendChild(beaconInfoTd); 137 | } 138 | 139 | this._addMenuItemShowBeacons(minionTr.dropdownmenu, pMinionId); 140 | } 141 | 142 | _addMenuItemShowBeacons (pMenu, pMinionId) { 143 | pMenu.addMenuItem("Show beacons", (pClickEvent) => { 144 | this.router.goTo("beacons-minion", {"minionid": pMinionId}, undefined, pClickEvent); 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/Events.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Output} from "../output/Output.js"; 4 | import {Panel} from "./Panel.js"; 5 | import {Utils} from "../Utils.js"; 6 | 7 | const MAX_EVENTS_IN_VIEW = 100; 8 | 9 | export class EventsPanel extends Panel { 10 | 11 | constructor () { 12 | super("events"); 13 | 14 | this.addTitle("Recent Events"); 15 | this.addSearchButton(); 16 | this.addPlayPauseButton(); 17 | this.addHelpButton([ 18 | "The content of this page is", 19 | "automatically refreshed.", 20 | "Display is limited to " + MAX_EVENTS_IN_VIEW + " events." 21 | ]); 22 | this.addTable(["Timestamp", "Tag", "Data"]); 23 | this.addMsg(); 24 | 25 | this.setPlayPauseButton("pause"); 26 | } 27 | 28 | onShow () { 29 | this.nrEvents = 0; 30 | this.updateFooter(); 31 | } 32 | 33 | updateFooter () { 34 | // when there are more than a screen-ful of events, the user 35 | // will not see the "press play" message. but the user already 36 | // knows that because that caused the events to be shown... 37 | let txt = Utils.txtZeroOneMany(this.nrEvents, "No events", "{0} event", "{0} events"); 38 | if (this.playOrPause === "play") { 39 | const tbody = this.table.tBodies[0]; 40 | if (tbody.rows.length) { 41 | txt += ", waiting for more events"; 42 | } else { 43 | txt += ", waiting for events"; 44 | } 45 | } 46 | super.updateFooter(txt); 47 | } 48 | 49 | handleAnyEvent (pTag, pData) { 50 | 51 | if (this.playOrPause !== "play") { 52 | return; 53 | } 54 | 55 | const tbody = this.table.tBodies[0]; 56 | const tr = Utils.createTr(); 57 | 58 | // add timestamp value 59 | const stampTd = Utils.createTd(); 60 | const stampSpan = Utils.createSpan(); 61 | let stampTxt = pData["_stamp"]; 62 | if (!stampTxt) { 63 | stampTxt = new Date().toISOString(); 64 | } 65 | Output.dateTimeStr(stampTxt, stampSpan, "bottom-left", true); 66 | stampTd.appendChild(stampSpan); 67 | tr.append(stampTd); 68 | 69 | // add tag value 70 | const tagTd = Utils.createTd("", pTag); 71 | tr.append(tagTd); 72 | 73 | // add data value 74 | const pDataObj = {}; 75 | Object.assign(pDataObj, pData); 76 | delete pDataObj._stamp; 77 | const dataTd = Utils.createTd("event-data", Output.formatObject(pDataObj)); 78 | tr.append(dataTd); 79 | 80 | tbody.prepend(tr); 81 | 82 | const searchBlock = this.div.querySelector(".search-box"); 83 | Utils.hideShowTableSearchBar(searchBlock, this.table, "refresh"); 84 | 85 | // limit to MAX_EVENTS_IN_VIEW rows only 86 | while (tbody.rows.length > MAX_EVENTS_IN_VIEW) { 87 | tbody.deleteRow(tbody.rows.length - 1); 88 | } 89 | 90 | this.nrEvents = tbody.rows.length; 91 | this.updateFooter(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/GrainsMinion.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Character} from "../Character.js"; 4 | import {DropDownMenu} from "../DropDown.js"; 5 | import {Output} from "../output/Output.js"; 6 | import {Panel} from "./Panel.js"; 7 | import {Utils} from "../Utils.js"; 8 | 9 | export class GrainsMinionPanel extends Panel { 10 | 11 | constructor () { 12 | super("grains-minion"); 13 | 14 | this.addTitle("Grains on " + Character.HORIZONTAL_ELLIPSIS); 15 | this.addPanelMenu(); 16 | this._addPanelMenuItemGrainsSetValAdd(); 17 | this._addPanelMenuItemSaltUtilRefreshGrains(); 18 | 19 | this.addSearchButton(); 20 | if (Utils.getQueryParam("popup") !== "true") { 21 | this.addCloseButton(); 22 | } 23 | this.addWarningField(); 24 | this.addTable(["-menu-", "Name", "Value"]); 25 | this.setTableSortable("Name", "asc"); 26 | this.setTableClickable("cmd"); 27 | this.addMsg(); 28 | } 29 | 30 | onShow () { 31 | const minionId = decodeURIComponent(Utils.getQueryParam("minionid")); 32 | 33 | this.updateTitle("Grains on " + minionId); 34 | 35 | const useCacheGrains = Utils.getStorageItemBoolean("session", "use_cache_for_grains", false); 36 | this.setWarningText("info", useCacheGrains ? "the content of this screen is based on cached grains info, minion status or grain info may not be accurate" : ""); 37 | 38 | const localGrainsItemsPromise = useCacheGrains ? this.api.getRunnerCacheGrains(minionId) : this.api.getLocalGrainsItems(minionId); 39 | 40 | localGrainsItemsPromise.then((pLocalGrainsItemsData) => { 41 | this._handleLocalGrainsItems(pLocalGrainsItemsData, minionId); 42 | return true; 43 | }, (pLocalGrainsItemsMsg) => { 44 | this._handleLocalGrainsItems(JSON.stringify(pLocalGrainsItemsMsg), minionId); 45 | return false; 46 | }); 47 | } 48 | 49 | _handleLocalGrainsItems (pLocalGrainsItemsData, pMinionId) { 50 | if (this.showErrorRowInstead(pLocalGrainsItemsData)) { 51 | return; 52 | } 53 | 54 | const grains = pLocalGrainsItemsData.return[0][pMinionId]; 55 | if (this.showErrorRowInstead(grains)) { 56 | return; 57 | } 58 | 59 | if (grains === undefined) { 60 | this.setMsg("Unknown minion '" + pMinionId + "'"); 61 | return; 62 | } 63 | if (grains === false) { 64 | this.setMsg("Minion '" + pMinionId + "' did not answer"); 65 | return; 66 | } 67 | 68 | const grainNames = Object.keys(grains).sort(); 69 | for (const grainName of grainNames) { 70 | const grainTr = Utils.createTr(); 71 | 72 | const grainMenu = new DropDownMenu(grainTr, "smaller"); 73 | 74 | const grainNameTd = Utils.createTd("grain-name", grainName); 75 | grainTr.appendChild(grainNameTd); 76 | 77 | const grainValue = Output.formatObject(grains[grainName]); 78 | 79 | this._addMenuItemGrainsSetValUpdate(grainMenu, pMinionId, grainName, grains); 80 | this._addMenuItemGrainsAppendWhenNeeded(grainMenu, pMinionId, grainName, grainValue); 81 | this._addMenuItemGrainsDelKey(grainMenu, pMinionId, grainName, grains[grainName]); 82 | this._addMenuItemGrainsDelVal(grainMenu, pMinionId, grainName, grains[grainName]); 83 | 84 | // menu comes before this data on purpose 85 | const grainValueTd = Utils.createTd("grain-value", grainValue); 86 | grainTr.appendChild(grainValueTd); 87 | 88 | const tbody = this.table.tBodies[0]; 89 | tbody.appendChild(grainTr); 90 | 91 | grainTr.addEventListener("click", (pClickEvent) => { 92 | const cmdArr = ["grains.setval", grainName, grains[grainName]]; 93 | this.runCommand("", pMinionId, cmdArr); 94 | pClickEvent.stopPropagation(); 95 | }); 96 | } 97 | 98 | const txt = Utils.txtZeroOneMany(grainNames.length, 99 | "No grains", "{0} grain", "{0} grains"); 100 | this.setMsg(txt); 101 | } 102 | 103 | _addPanelMenuItemGrainsSetValAdd () { 104 | this.panelMenu.addMenuItem("Add grain...", () => { 105 | // use placeholders for name and value 106 | const minionId = decodeURIComponent(Utils.getQueryParam("minionid")); 107 | const cmdArr = ["grains.setval", "", ""]; 108 | this.runCommand("", minionId, cmdArr); 109 | }); 110 | } 111 | 112 | _addPanelMenuItemSaltUtilRefreshGrains () { 113 | this.panelMenu.addMenuItem("Refresh grains...", () => { 114 | const minionId = decodeURIComponent(Utils.getQueryParam("minionid")); 115 | const cmdArr = ["saltutil.refresh_grains"]; 116 | this.runCommand("", minionId, cmdArr); 117 | }); 118 | } 119 | 120 | _addMenuItemGrainsSetValUpdate (pMenu, pMinionId, key, grains) { 121 | pMenu.addMenuItem("Edit grain...", () => { 122 | const cmdArr = ["grains.setval", key, grains[key]]; 123 | this.runCommand("", pMinionId, cmdArr); 124 | }); 125 | } 126 | 127 | _addMenuItemGrainsAppendWhenNeeded (pMenu, pMinionId, key, pGrainValue) { 128 | if (!pGrainValue.startsWith("[")) { 129 | return; 130 | } 131 | pMenu.addMenuItem("Add value...", () => { 132 | const cmdArr = ["grains.append", key, ""]; 133 | this.runCommand("", pMinionId, cmdArr); 134 | }); 135 | } 136 | 137 | _addMenuItemGrainsDelKey (pMenu, pMinionId, pKey, pValue) { 138 | const cmdArr = ["grains.delkey"]; 139 | if (typeof pValue === "object") { 140 | cmdArr.push("force=", true); 141 | } 142 | cmdArr.push(pKey); 143 | pMenu.addMenuItem("Delete key...", () => { 144 | this.runCommand("", pMinionId, cmdArr); 145 | }); 146 | } 147 | 148 | _addMenuItemGrainsDelVal (pMenu, pMinionId, pKey, pValue) { 149 | const cmdArr = ["grains.delval"]; 150 | if (typeof pValue === "object") { 151 | cmdArr.push("force=", true); 152 | } 153 | cmdArr.push(pKey); 154 | pMenu.addMenuItem("Delete value...", () => { 155 | this.runCommand("", pMinionId, cmdArr); 156 | }); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/Issues.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {BeaconsIssues} from "../issues/Beacons.js"; 4 | import {JobsRunningIssues} from "../issues/JobsRunning.js"; 5 | import {KeysIssues} from "../issues/Keys.js"; 6 | import {NotConnectedIssues} from "../issues/NotConnected.js"; 7 | import {Panel} from "./Panel.js"; 8 | import {SchedulesIssues} from "../issues/Schedules.js"; 9 | import {StateIssues} from "../issues/State.js"; 10 | import {Utils} from "../Utils.js"; 11 | 12 | export class IssuesPanel extends Panel { 13 | 14 | constructor () { 15 | super("issues"); 16 | 17 | this.addTitle("Issues"); 18 | this.addSearchButton(); 19 | this.addPlayPauseButton(); 20 | this.addHelpButton([ 21 | "This page contains an overview of problems", 22 | "that are observed in various categories." 23 | ]); 24 | this.addTable(["-menu-", "Description"]); 25 | this.setTableClickable("cmd"); 26 | this.addMsg(); 27 | 28 | // keep the list of "loading..." messages 29 | const msg2 = Utils.createDiv(); 30 | this.div.appendChild(msg2); 31 | this.msg2 = msg2; 32 | 33 | // cannot use this now since we are loading 34 | // the data in random order 35 | // this.setTableSortable("Description", "asc"); 36 | 37 | this.keysIssues = new KeysIssues(); 38 | this.jobsIssues = new JobsRunningIssues(); 39 | this.beaconsIssues = new BeaconsIssues(); 40 | this.schedulesIssues = new SchedulesIssues(); 41 | this.notConnectedIssues = new NotConnectedIssues(); 42 | this.lowStateIssues = new StateIssues(); 43 | } 44 | 45 | updateFooter () { 46 | const txt = this.issuesStatus; 47 | super.updateFooter(txt || "(loading)"); 48 | } 49 | 50 | onShow () { 51 | const p1 = this.keysIssues.onGetIssues(this); 52 | const p2 = this.jobsIssues.onGetIssues(this); 53 | const p3 = this.beaconsIssues.onGetIssues(this); 54 | const p4 = this.schedulesIssues.onGetIssues(this); 55 | const p5 = this.notConnectedIssues.onGetIssues(this); 56 | const p6 = this.lowStateIssues.onGetIssues(this); 57 | 58 | /* eslint-disable compat/compat */ 59 | /* Promise.all is not supported in op_mini all, IE 11 */ 60 | const allPromise = Promise.all([p1, p2, p3, p4, p5, p6]); 61 | /* eslint-enable compat/compat */ 62 | allPromise.then(() => { 63 | // VOID 64 | }, (pErrorMsg) => { 65 | this.setMsg("(error)"); 66 | Utils.addToolTip(this.msgDiv, pErrorMsg); 67 | }); 68 | } 69 | 70 | onHide () { 71 | // from StateIssues 72 | this.jobs = null; 73 | 74 | if (this.issuesStateTimeout) { 75 | // stop the timer when nobody is looking 76 | window.clearTimeout(this.issuesStateTimeout); 77 | this.issuesStateTimeout = null; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/JobsSummary.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Character} from "../Character.js"; 4 | import {DropDownMenu} from "../DropDown.js"; 5 | import {JobsPanel} from "./Jobs.js"; 6 | import {Output} from "../output/Output.js"; 7 | import {TargetType} from "../TargetType.js"; 8 | import {Utils} from "../Utils.js"; 9 | 10 | // how many jobs to load in the side panel 11 | const MAX_JOBS_SUMMARY = 7; 12 | 13 | export class JobsSummaryPanel extends JobsPanel { 14 | 15 | constructor () { 16 | super("jobs"); 17 | 18 | this.addTitle("Recent Jobs"); 19 | this.addSearchButton(); 20 | this.addTable(["-dummy-", "-dummy-"]); 21 | this.setTableClickable("page"); 22 | this.addMsg(); 23 | } 24 | 25 | onShow () { 26 | super.onShow(MAX_JOBS_SUMMARY); 27 | } 28 | 29 | /* eslint-disable class-methods-use-this */ 30 | jobsListIsReady () { 31 | // VOID 32 | } 33 | /* eslint-enable class-methods-use-this */ 34 | 35 | addJob (job) { 36 | const tr = Utils.createTr(); 37 | tr.id = Utils.getIdFromJobId(job.id); 38 | 39 | // menu on left side to prevent it from going past end of window 40 | const menu = new DropDownMenu(tr, "smaller"); 41 | 42 | const td = Utils.createTd(); 43 | 44 | let targetText = TargetType.makeTargetText(job); 45 | const maxTextLength = 50; 46 | if (targetText.length > maxTextLength) { 47 | // prevent column becoming too wide 48 | targetText = targetText.substring(0, maxTextLength) + Character.HORIZONTAL_ELLIPSIS; 49 | } 50 | const targetDiv = Utils.createDiv("target", targetText); 51 | td.appendChild(targetDiv); 52 | 53 | const functionText = job.Function; 54 | const functionDiv = Utils.createDiv("function", functionText); 55 | td.appendChild(functionDiv); 56 | 57 | const statusSpan = Utils.createSpan(["job-status", "no-job-status"], "loading" + Character.HORIZONTAL_ELLIPSIS); 58 | // effectively also the whole column, but it does not look like a column on screen 59 | statusSpan.addEventListener("click", (pClickEvent) => { 60 | // show "loading..." only once, but we are updating the whole column 61 | statusSpan.classList.add("no-job-status"); 62 | statusSpan.innerText = "loading" + Character.HORIZONTAL_ELLIPSIS; 63 | this.startRunningJobs(); 64 | pClickEvent.stopPropagation(); 65 | }); 66 | td.appendChild(statusSpan); 67 | 68 | const startTimeDiv = Utils.createDiv("time"); 69 | const startTimeSpan = Utils.createSpan(); 70 | Output.dateTimeStr(job.StartTime, startTimeSpan); 71 | startTimeDiv.appendChild(startTimeSpan); 72 | td.appendChild(startTimeDiv); 73 | 74 | tr.appendChild(td); 75 | 76 | // complete the menu 77 | this._addMenuItemShowDetails(menu, job); 78 | this._addMenuItemUpdateStatus(menu, statusSpan); 79 | 80 | const tbody = this.table.tBodies[0]; 81 | tbody.appendChild(tr); 82 | 83 | tr.addEventListener("click", (pClickEvent) => { 84 | this.router.goTo("job", {"id": job.id}, undefined, pClickEvent); 85 | pClickEvent.stopPropagation(); 86 | }); 87 | } 88 | 89 | _addMenuItemShowDetails (pMenu, job) { 90 | pMenu.addMenuItem("Show details", (pClickEvent) => { 91 | this.router.goTo("job", {"id": job.id}, undefined, pClickEvent); 92 | }); 93 | } 94 | 95 | _addMenuItemUpdateStatus (pMenu, statusSpan) { 96 | pMenu.addMenuItem("Update status", () => { 97 | statusSpan.classList.add("no-job-status"); 98 | statusSpan.innerText = "loading" + Character.HORIZONTAL_ELLIPSIS; 99 | this.startRunningJobs(); 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/Pillars.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Panel} from "./Panel.js"; 4 | import {Utils} from "../Utils.js"; 5 | 6 | export class PillarsPanel extends Panel { 7 | 8 | constructor () { 9 | super("pillars"); 10 | 11 | this.addTitle("Pillars"); 12 | this.addSearchButton(); 13 | this.addWarningField(); 14 | this.addTable(["-menu-", "Minion", "Status", "Pillars"]); 15 | this.setTableSortable("Minion", "asc"); 16 | this.setTableClickable("page"); 17 | this.addMsg(); 18 | } 19 | 20 | onShow () { 21 | const useCachePillar = Utils.getStorageItemBoolean("session", "use_cache_for_pillar", false); 22 | this.setWarningText("info", useCachePillar ? "the content of this screen is based on cached grains info, minion status or pillar info may not be accurate" : ""); 23 | 24 | const wheelKeyListAllPromise = this.api.getWheelKeyListAll(); 25 | const localPillarObfuscatePromise = useCachePillar ? this.api.getRunnerCachePillar(null) : this.api.getLocalPillarObfuscate(null); 26 | 27 | this.nrMinions = 0; 28 | 29 | wheelKeyListAllPromise.then((pWheelKeyListAllData) => { 30 | this._handlePillarsWheelKeyListAll(pWheelKeyListAllData); 31 | localPillarObfuscatePromise.then((pLocalPillarObfuscateData) => { 32 | this.updateMinions(pLocalPillarObfuscateData); 33 | return true; 34 | }, (pLocalPillarObfuscateMsg) => { 35 | const allMinionsErr = Utils.msgPerMinion(pWheelKeyListAllData.return[0].data.return.minions, JSON.stringify(pLocalPillarObfuscateMsg)); 36 | this.updateMinions({"return": [allMinionsErr]}); 37 | return false; 38 | }); 39 | return true; 40 | }, (pWheelKeyListAllMsg) => { 41 | this._handlePillarsWheelKeyListAll(JSON.stringify(pWheelKeyListAllMsg)); 42 | Utils.ignorePromise(localPillarObfuscatePromise); 43 | return false; 44 | }); 45 | } 46 | 47 | _handlePillarsWheelKeyListAll (pWheelKeyListAllData) { 48 | if (this.showErrorRowInstead(pWheelKeyListAllData)) { 49 | return; 50 | } 51 | 52 | const keys = pWheelKeyListAllData.return[0].data.return; 53 | this.nrMinions = keys.minions.length; 54 | this.nrUnaccepted = keys.minions_pre.length; 55 | 56 | const minionIds = keys.minions.sort(); 57 | for (const minionId of minionIds) { 58 | const minionTr = this.addMinion(minionId); 59 | 60 | // preliminary dropdown menu 61 | this._addMenuItemShowPillars(minionTr.dropdownmenu, minionId); 62 | 63 | minionTr.addEventListener("click", (pClickEvent) => { 64 | this.router.goTo("pillars-minion", {"minionid": minionId}, undefined, pClickEvent); 65 | pClickEvent.stopPropagation(); 66 | }); 67 | } 68 | 69 | this.updateFooter(); 70 | } 71 | 72 | updateOfflineMinion (pMinionId, pMinionsDict) { 73 | super.updateOfflineMinion(pMinionId, pMinionsDict); 74 | 75 | const minionTr = this.table.querySelector("#" + Utils.getIdFromMinionId(pMinionId)); 76 | 77 | // force same columns on all rows 78 | minionTr.appendChild(Utils.createTd("pillarinfo")); 79 | } 80 | 81 | updateMinion (pMinionData, pMinionId, pAllMinionsGrains) { 82 | super.updateMinion(null, pMinionId, pAllMinionsGrains); 83 | 84 | const minionTr = this.table.querySelector("#" + Utils.getIdFromMinionId(pMinionId)); 85 | 86 | let cnt; 87 | let pillarInfoText; 88 | if (typeof pMinionData === "object") { 89 | cnt = Object.keys(pMinionData).length; 90 | pillarInfoText = Utils.txtZeroOneMany(cnt, 91 | "no pillars", "{0} pillar", "{0} pillars"); 92 | } else { 93 | cnt = -1; 94 | pillarInfoText = ""; 95 | } 96 | const pillarInfoTd = Utils.createTd("pillarinfo", pillarInfoText); 97 | pillarInfoTd.setAttribute("sorttable_customkey", cnt); 98 | if (typeof pMinionData !== "object") { 99 | Utils.addErrorToTableCell(pillarInfoTd, pMinionData); 100 | } 101 | minionTr.appendChild(pillarInfoTd); 102 | 103 | this._addMenuItemShowPillars(minionTr.dropdownmenu, pMinionId); 104 | } 105 | 106 | _addMenuItemShowPillars (pMenu, pMinionId) { 107 | pMenu.addMenuItem("Show pillars", (pClickEvent) => { 108 | this.router.goTo("pillars-minion", {"minionid": pMinionId}, undefined, pClickEvent); 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/PillarsMinion.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Character} from "../Character.js"; 4 | import {Output} from "../output/Output.js"; 5 | import {OutputYaml} from "../output/OutputYaml.js"; 6 | import {Panel} from "./Panel.js"; 7 | import {Utils} from "../Utils.js"; 8 | 9 | export class PillarsMinionPanel extends Panel { 10 | 11 | constructor () { 12 | super("pillars-minion"); 13 | 14 | this.addTitle("Pillars on " + Character.HORIZONTAL_ELLIPSIS); 15 | this.addPanelMenu(); 16 | this._addPanelMenuItemSaltUtilRefreshPillar(); 17 | this.addSearchButton(); 18 | if (Utils.getQueryParam("popup") !== "true") { 19 | this.addCloseButton(); 20 | } 21 | this.addHelpButton([ 22 | "The content of specific well-known pillar values can be made visible", 23 | "automatically by configuring their name in the server-side configuration file.", 24 | "See README.md for more details." 25 | ]); 26 | this.addWarningField(); 27 | this.addTable(["Name", "Value"]); 28 | this.setTableSortable("Name", "asc"); 29 | this.addMsg(); 30 | } 31 | 32 | onShow () { 33 | const minionId = decodeURIComponent(Utils.getQueryParam("minionid")); 34 | 35 | this.updateTitle("Pillars on " + minionId); 36 | 37 | const useCachePillar = Utils.getStorageItemBoolean("session", "use_cache_for_pillar", false); 38 | this.setWarningText("info", useCachePillar ? "the content of this screen is based on cached grains info, minion status or pillar info may not be accurate" : ""); 39 | 40 | const localPillarItemsPromise = useCachePillar ? this.api.getRunnerCachePillar(minionId) : this.api.getLocalPillarItems(minionId); 41 | 42 | localPillarItemsPromise.then((pLocalPillarItemsData) => { 43 | this._handleLocalPillarItems(pLocalPillarItemsData, minionId); 44 | return true; 45 | }, (pLocalPillarItemsMsg) => { 46 | this._handleLocalPillarItems(JSON.stringify(pLocalPillarItemsMsg), minionId); 47 | return false; 48 | }); 49 | } 50 | 51 | _handleLocalPillarItems (pLocalPillarItemsData, pMinionId) { 52 | if (this.showErrorRowInstead(pLocalPillarItemsData)) { 53 | return; 54 | } 55 | 56 | const pillars = pLocalPillarItemsData.return[0][pMinionId]; 57 | if (this.showErrorRowInstead(pillars)) { 58 | return; 59 | } 60 | 61 | if (pillars === undefined) { 62 | this.setMsg("Unknown minion '" + pMinionId + "'"); 63 | return; 64 | } 65 | if (pillars === false) { 66 | this.setMsg("Minion '" + pMinionId + "' did not answer"); 67 | return; 68 | } 69 | 70 | // collect the public pillars and compile their regexps 71 | const publicPillars = Utils.getStorageItemList("session", "public_pillars"); 72 | for (let i = 0; i < publicPillars.length; i++) { 73 | try { 74 | publicPillars[i] = new RegExp(publicPillars[i]); 75 | } catch (err) { 76 | // most likely a syntax error in the RE 77 | Utils.error("error in regexp saltgui_public_pillars[" + i + "]=" + OutputYaml.formatYAML(publicPillars[i]) + " --> " + err.name + ": " + err.message); 78 | publicPillars[i] = null; 79 | } 80 | } 81 | 82 | const keys = Object.keys(pillars).sort(); 83 | for (const pillarName of keys) { 84 | const pillar = Utils.createTr(); 85 | 86 | const nameTd = Utils.createTd("pillar-name", pillarName); 87 | pillar.appendChild(nameTd); 88 | 89 | // menu comes before this data if there was any 90 | 91 | const pillarValueTd = Utils.createTd(); 92 | 93 | const pillarValueHidden = Character.BLACK_CIRCLE.repeat(8); 94 | const pillarHiddenDiv = Utils.createDiv("pillar-hidden", pillarValueHidden); 95 | pillarHiddenDiv.style.display = "inline-block"; 96 | Utils.addToolTip(pillarHiddenDiv, "Click to show"); 97 | // initially use the hidden view 98 | pillarValueTd.appendChild(pillarHiddenDiv); 99 | 100 | const pillarValueShown = Output.formatObject(pillars[pillarName]); 101 | const pillarShownDiv = Utils.createDiv("pillar-shown", pillarValueShown); 102 | // initially hide the normal view 103 | pillarShownDiv.style.display = "none"; 104 | Utils.addToolTip(pillarShownDiv, "Click to hide"); 105 | // add the non-masked representation, not shown yet 106 | pillarValueTd.appendChild(pillarShownDiv); 107 | 108 | // show public pillars immediatelly 109 | for (const publicPillar of publicPillars) { 110 | if (publicPillar && publicPillar.test(pillarName)) { 111 | // same code as when clicking the hidden value 112 | pillarHiddenDiv.style.display = "none"; 113 | pillarShownDiv.style.display = "inline-block"; 114 | break; 115 | } 116 | } 117 | 118 | pillar.appendChild(pillarValueTd); 119 | 120 | pillarHiddenDiv.addEventListener("click", (pClickEvent) => { 121 | pillarHiddenDiv.style.display = "none"; 122 | pillarShownDiv.style.display = "inline-block"; 123 | pClickEvent.stopPropagation(); 124 | }); 125 | 126 | pillarShownDiv.addEventListener("click", (pClickEvent) => { 127 | pillarShownDiv.style.display = "none"; 128 | pillarHiddenDiv.style.display = "inline-block"; 129 | pClickEvent.stopPropagation(); 130 | }); 131 | 132 | const tbody = this.table.tBodies[0]; 133 | tbody.appendChild(pillar); 134 | } 135 | 136 | const txt = Utils.txtZeroOneMany(keys.length, 137 | "No pillars", "{0} pillar", "{0} pillars"); 138 | this.setMsg(txt); 139 | } 140 | 141 | _addPanelMenuItemSaltUtilRefreshPillar () { 142 | this.panelMenu.addMenuItem("Refresh pillar...", () => { 143 | const minionId = decodeURIComponent(Utils.getQueryParam("minionid")); 144 | const cmdArr = ["saltutil.refresh_pillar"]; 145 | this.runCommand("", minionId, cmdArr); 146 | }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/Reactors.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Output} from "../output/Output.js"; 4 | import {Panel} from "./Panel.js"; 5 | import {Router} from "../Router.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class ReactorsPanel extends Panel { 9 | 10 | constructor () { 11 | super("reactors"); 12 | 13 | this.addTitle("Reactors"); 14 | this.addSearchButton(); 15 | this.addTable(["Event", "Reactors"]); 16 | this.setTableSortable("Event", "asc"); 17 | 18 | this.addMsg(); 19 | } 20 | 21 | onShow () { 22 | const wheelConfigValuesPromise = this.api.getWheelConfigValues(); 23 | 24 | wheelConfigValuesPromise.then((pWheelConfigValuesData) => { 25 | this._handleReactorsWheelConfigValues(pWheelConfigValuesData); 26 | return true; 27 | }, (pWheelConfigValuesMsg) => { 28 | this._handleReactorsWheelConfigValues(JSON.stringify(pWheelConfigValuesMsg)); 29 | return false; 30 | }); 31 | } 32 | 33 | _handleReactorsWheelConfigValues (pWheelConfigValuesData) { 34 | if (this.showErrorRowInstead(pWheelConfigValuesData)) { 35 | return; 36 | } 37 | 38 | // should we update it or just use from cache (see commandbox) ? 39 | let reactorsArr = pWheelConfigValuesData.return[0].data.return.reactor; 40 | if (reactorsArr) { 41 | Utils.setStorageItem("session", "reactors", JSON.stringify(reactorsArr)); 42 | Router.updateMainMenu(); 43 | } else { 44 | reactorsArr = []; 45 | } 46 | 47 | // the reactors are organized as an array of maps 48 | // first re-organize into a single map 49 | const reactorsMap = {}; 50 | for (const reactor of reactorsArr) { 51 | for (const eventTag in reactor) { 52 | reactorsMap[eventTag] = reactor[eventTag]; 53 | } 54 | } 55 | 56 | // then populate the table 57 | for (const eventTag of Object.keys(reactorsMap).sort()) { 58 | this._addReactor(eventTag, reactorsMap[eventTag]); 59 | } 60 | 61 | const txt = Utils.txtZeroOneMany(reactorsArr.length, 62 | "No reactors", "{0} reactor", "{0} reactors"); 63 | this.setMsg(txt); 64 | } 65 | 66 | _addReactor (pEvent, pReactor) { 67 | const tr = Utils.createTr(); 68 | tr.appendChild(Utils.createTd("", pEvent)); 69 | tr.appendChild(Utils.createTd("", Output.formatObject(pReactor))); 70 | 71 | const tbody = this.table.tBodies[0]; 72 | tbody.appendChild(tr); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/Schedules.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Panel} from "./Panel.js"; 4 | import {Utils} from "../Utils.js"; 5 | 6 | export class SchedulesPanel extends Panel { 7 | 8 | constructor () { 9 | super("schedules"); 10 | 11 | this.addTitle("Schedules"); 12 | this.addSearchButton(); 13 | this.addTable(["-menu-", "Minion", "Status", "Schedules"]); 14 | this.setTableSortable("Minion", "asc"); 15 | this.setTableClickable("page"); 16 | this.addMsg(); 17 | } 18 | 19 | onShow () { 20 | const wheelKeyListAllPromise = this.api.getWheelKeyListAll(); 21 | const localScheduleListPromise = this.api.getLocalScheduleList(null); 22 | 23 | this.nrMinions = 0; 24 | 25 | wheelKeyListAllPromise.then((pWheelKeyListAllData) => { 26 | this._handleSchedulesWheelKeyListAll(pWheelKeyListAllData); 27 | localScheduleListPromise.then((pLocalScheduleListData) => { 28 | this.updateMinions(pLocalScheduleListData); 29 | return true; 30 | }, (pLocalScheduleListMsg) => { 31 | const allMinionsErr = Utils.msgPerMinion(pWheelKeyListAllData.return[0].data.return.minions, JSON.stringify(pLocalScheduleListMsg)); 32 | this.updateMinions({"return": [allMinionsErr]}); 33 | return false; 34 | }); 35 | return true; 36 | }, (pWheelKeyListAllMsg) => { 37 | this._handleSchedulesWheelKeyListAll(JSON.stringify(pWheelKeyListAllMsg)); 38 | Utils.ignorePromise(localScheduleListPromise); 39 | return false; 40 | }); 41 | } 42 | 43 | // This one has some historic ballast: 44 | // Meta-data is returned on the same level as 45 | // the list of scheduled items 46 | static fixSchedulesMinion (pData) { 47 | if (typeof pData !== "object") { 48 | return pData; 49 | } 50 | 51 | const ret = {"enabled": true, "schedules": {}}; 52 | 53 | for (const scheduleName in pData) { 54 | // "enabled" is always a boolean (when present) 55 | if (scheduleName === "enabled") { 56 | ret.enabled = pData.enabled; 57 | continue; 58 | } 59 | 60 | // correct for empty list that returns this dummy value 61 | if (scheduleName === "schedule" && JSON.stringify(pData[scheduleName]) === "{}") { 62 | continue; 63 | } 64 | 65 | ret.schedules[scheduleName] = pData[scheduleName]; 66 | 67 | // Since 2019.02, splay is always added, even when not set 68 | // so remove it when it has an empty value 69 | if (ret.schedules[scheduleName]["splay"] === null) { 70 | delete ret.schedules[scheduleName]["splay"]; 71 | } 72 | } 73 | 74 | return ret; 75 | } 76 | 77 | _handleSchedulesWheelKeyListAll (pWheelKeyListAllData) { 78 | if (this.showErrorRowInstead(pWheelKeyListAllData)) { 79 | return; 80 | } 81 | 82 | const keys = pWheelKeyListAllData.return[0].data.return; 83 | this.nrMinions = keys.minions.length; 84 | this.nrUnaccepted = keys.minions_pre.length; 85 | 86 | const minionIds = keys.minions.sort(); 87 | for (const minionId of minionIds) { 88 | const minionTr = this.addMinion(minionId); 89 | 90 | // preliminary dropdown menu 91 | this._addMenuItemShowSchedules(minionTr.dropdownmenu, minionId); 92 | 93 | minionTr.addEventListener("click", (pClickEvent) => { 94 | this.router.goTo("schedules-minion", {"minionid": minionId}, undefined, pClickEvent); 95 | pClickEvent.stopPropagation(); 96 | }); 97 | } 98 | 99 | this.updateFooter(); 100 | } 101 | 102 | updateOfflineMinion (pMinionId, pMinionsDict) { 103 | super.updateOfflineMinion(pMinionId, pMinionsDict); 104 | 105 | const minionTr = this.table.querySelector("#" + Utils.getIdFromMinionId(pMinionId)); 106 | 107 | // force same columns on all rows 108 | minionTr.appendChild(Utils.createTd("scheduleinfo")); 109 | } 110 | 111 | updateMinion (pMinionData, pMinionId, pAllMinionsGrains) { 112 | 113 | pMinionData = SchedulesPanel.fixSchedulesMinion(pMinionData); 114 | 115 | super.updateMinion(pMinionData, pMinionId, pAllMinionsGrains); 116 | 117 | const minionTr = this.getElement(Utils.getIdFromMinionId(pMinionId)); 118 | 119 | minionTr.appendChild(Utils.createTd("minion-id", pMinionId)); 120 | 121 | const statusDiv = Utils.createTd(["status", "accepted"], "accepted"); 122 | minionTr.appendChild(statusDiv); 123 | 124 | let cnt; 125 | let scheduleinfo; 126 | if (typeof pMinionData === "object") { 127 | cnt = Object.keys(pMinionData.schedules).length; 128 | scheduleinfo = Utils.txtZeroOneMany(cnt, 129 | "no schedules", "{0} schedule", "{0} schedules"); 130 | if (!pMinionData.enabled) { 131 | scheduleinfo += " (disabled)"; 132 | } 133 | } else { 134 | cnt = -1; 135 | scheduleinfo = ""; 136 | } 137 | 138 | const td = Utils.createTd("scheduleinfo", scheduleinfo); 139 | if (typeof pMinionData !== "object") { 140 | Utils.addErrorToTableCell(td, pMinionData); 141 | } 142 | td.setAttribute("sorttable_customkey", cnt); 143 | minionTr.appendChild(td); 144 | 145 | // final dropdownmenu 146 | this._addMenuItemShowSchedules(minionTr.dropdownmenu, pMinionId); 147 | } 148 | 149 | _addMenuItemShowSchedules (pMenu, pMinionId) { 150 | pMenu.addMenuItem("Show schedules", (pClickEvent) => { 151 | this.router.goTo("schedules-minion", {"minionid": pMinionId}, undefined, pClickEvent); 152 | }); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /saltgui/static/scripts/panels/Stats.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import {Character} from "../Character.js"; 4 | import {Output} from "../output/Output.js"; 5 | import {Panel} from "./Panel.js"; 6 | import {Utils} from "../Utils.js"; 7 | 8 | export class StatsPanel extends Panel { 9 | 10 | constructor () { 11 | super("stats"); 12 | 13 | this.addTitle("Stats"); 14 | this.addHelpButton([ 15 | "Numeric fields representing a timestamp are visible as string.", 16 | "Numeric fields representing a duration are visible as string.", 17 | "Trivial information on worker threads may have been removed." 18 | ]); 19 | this.addTable(["/stats"]); 20 | this.addMsg(); 21 | } 22 | 23 | onShow () { 24 | if (this.table.tBodies[0].children.length === 0) { 25 | // cannot do this in the constructor 26 | // since the framework removes all rows 27 | const tr = Utils.createTr(); 28 | this.table.tBodies[0].appendChild(tr); 29 | const td = Utils.createTd(); 30 | tr.appendChild(td); 31 | this.statsTd = td; 32 | } 33 | 34 | this.onShowNow(); 35 | 36 | this.updateStatsInterval = window.setInterval(() => { 37 | this.onShowNow(); 38 | }, 5000); 39 | } 40 | 41 | onShowNow () { 42 | const statsPromise = this.api.getStats(); 43 | 44 | statsPromise.then((pStatsData) => { 45 | this._handleStats(pStatsData); 46 | return true; 47 | }, (pStatsMsg) => { 48 | this._handleStats(JSON.stringify(pStatsMsg)); 49 | return false; 50 | }); 51 | } 52 | 53 | onHide () { 54 | if (this.updateStatsInterval) { 55 | // stop the timer when nobody is looking 56 | window.clearInterval(this.updateStatsInterval); 57 | this.updateStatsInterval = null; 58 | } 59 | } 60 | 61 | // provide a shortened date format for cases 62 | // where we see the timezone multiple times on one screen 63 | static _explainDateTime (pDateTimeInMs) { 64 | if (pDateTimeInMs === null) { 65 | return pDateTimeInMs; 66 | } 67 | return pDateTimeInMs + " (=" + Output.dateTimeStr(pDateTimeInMs) + ")"; 68 | } 69 | 70 | _handleStats (pStatsData) { 71 | if (this.showErrorRowInstead(pStatsData)) { 72 | this.statsTd.innerHTML = "this error is typically caused by using the collect_stats: True setting in the master configuration file, which is broken in at least the recent versions of salt-api"; 73 | window.clearInterval(this.updateStatsInterval); 74 | this.updateStatsInterval = null; 75 | return; 76 | } 77 | 78 | this.setMsg(null); 79 | 80 | for (const topKey in pStatsData) { 81 | 82 | // this section should not contain threads 83 | if (topKey === "CherryPy Applications") { 84 | continue; 85 | } 86 | 87 | // loop over all threads 88 | const workerThreads = pStatsData[topKey]["Worker Threads"]; 89 | if (!workerThreads) { 90 | continue; 91 | } 92 | 93 | let first = true; 94 | /* eslint-disable no-labels */ 95 | nextThread: for (const threadName of Object.keys(workerThreads).sort((aa, bb) => aa.localeCompare(bb, "en", {"numeric": true}))) { 96 | if (first) { 97 | // always show the first item 98 | // so that the structure is known even when all threads show zeroes 99 | first = false; 100 | continue; 101 | } 102 | const thread = workerThreads[threadName]; 103 | // find threads with all-zero statistics 104 | for (const counterName in thread) { 105 | if (counterName === "Enabled") { 106 | // not a counter 107 | continue; 108 | } 109 | if (thread[counterName] !== 0) { 110 | continue nextThread; 111 | } 112 | } 113 | // thread has all-zero statistics, remove that part 114 | workerThreads[threadName] = Character.HORIZONTAL_ELLIPSIS; 115 | } 116 | /* eslint-enable no-labels */ 117 | } 118 | 119 | const appData = pStatsData["CherryPy Applications"]; 120 | if (appData) { 121 | // annotate 3 fields that have a huge integer value 122 | // this turns the fields into strings (was number) 123 | // we'll ignore that now 124 | 125 | appData["Current Time"] = StatsPanel._explainDateTime(appData["Current Time"]); 126 | 127 | appData["Start Time"] = StatsPanel._explainDateTime(appData["Start Time"]); 128 | 129 | appData["Uptime"] = StatsPanel._explainDateTime(appData["Uptime"]); 130 | 131 | const requests = appData["Requests"]; 132 | for (const key in requests) { 133 | requests[key]["Start Time"] = StatsPanel._explainDateTime(requests[key]["Start Time"]); 134 | requests[key]["End Time"] = StatsPanel._explainDateTime(requests[key]["End Time"]); 135 | } 136 | 137 | const slowQueries = appData["Slow Queries"]; 138 | for (const key in slowQueries) { 139 | slowQueries[key]["Start Time"] = StatsPanel._explainDateTime(slowQueries[key]["Start Time"]); 140 | slowQueries[key]["End Time"] = StatsPanel._explainDateTime(slowQueries[key]["End Time"]); 141 | } 142 | } 143 | 144 | this.statsTd.innerText = Output.formatObject(pStatsData); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /saltgui/static/sorttable/sorttable.css: -------------------------------------------------------------------------------- 1 | .sorttable_sortable { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/beacons.css: -------------------------------------------------------------------------------- 1 | .beacon-config { 2 | white-space: pre; 3 | } 4 | 5 | .beacon-value { 6 | white-space: pre; 7 | } 8 | 9 | #page-beacons { 10 | width: 100%; 11 | } 12 | 13 | .beacons { 14 | padding: 0; 15 | } 16 | 17 | .beacon-disabled, 18 | .beacon-waiting { 19 | color: gray; 20 | } 21 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/controls.css: -------------------------------------------------------------------------------- 1 | /* css for controls */ 2 | 3 | input, 4 | select { 5 | color: #272727; 6 | padding: 7px 10px; 7 | display: inline-block; 8 | margin-bottom: 15px; 9 | font-size: 14px; 10 | width: 60%; 11 | min-width: 200px; 12 | } 13 | 14 | input[type="text"], 15 | input[type="password"], 16 | select { 17 | border: 2px solid #e2e2e2; 18 | border-radius: 2px; 19 | } 20 | 21 | input[type="text"]:focus, 22 | input[type="password"]:focus, 23 | select:focus { 24 | border: 2px solid #4caf50; 25 | } 26 | 27 | input[type="submit"] { 28 | background-color: #4caf50; 29 | color: white; 30 | border: 0; 31 | margin-top: 5px; 32 | margin-bottom: 0; 33 | cursor: pointer; 34 | box-shadow: 0 0 5px rgba(33, 33, 33, 50%); 35 | width: 20%; 36 | min-width: 200px; 37 | } 38 | 39 | input[type="submit"]:active:hover { 40 | box-shadow: 0 0 0; 41 | } 42 | 43 | input:disabled { 44 | cursor: default; 45 | opacity: 0.2; 46 | } 47 | 48 | /* end */ 49 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/dropdown.css: -------------------------------------------------------------------------------- 1 | /* The container
- needed to position the menu-dropdown content */ 2 | .menu-dropdown { 3 | cursor: default; 4 | } 5 | 6 | div.search-box .menu-dropdown { 7 | margin: 0 10px 0 0; 8 | } 9 | 10 | /* Dropdown Content (Hidden by Default) */ 11 | .run-command-button .menu-dropdown-content { 12 | display: none; 13 | position: absolute; 14 | max-height: 300px; 15 | overflow-y: auto; 16 | background: #fff; 17 | box-shadow: 0 3px 10px -2px rgba(0, 0, 0, 30%); 18 | border: 1px solid rgba(0, 0, 0, 10%); 19 | z-index: 5; 20 | cursor: pointer; 21 | text-align: left; 22 | } 23 | 24 | /* Links inside the menu-dropdown */ 25 | .dropdown .dropdown-content div:not(.menu-item-hidden), 26 | .run-command-button .menu-dropdown-content div:not(.menu-item-hidden) { 27 | display: block; 28 | float: left; 29 | clear: both; 30 | width: 100%; 31 | padding: 15px 30px; 32 | margin: 0; 33 | -webkit-transition: all 0.2s ease-in-out; 34 | -moz-transition: all 0.2s ease-in-out; 35 | transition: all 0.2s ease-in-out; 36 | } 37 | 38 | /* Change color of menu-dropdown links on hover */ 39 | .run-command-button .menu-dropdown-content div.run-command-button:hover { 40 | background: rgba(0, 0, 0, 15%); 41 | cursor: pointer; 42 | } 43 | 44 | /* Show the menu-dropdown menu on hover */ 45 | .run-command-button:hover .menu-dropdown-content { 46 | display: block; 47 | } 48 | 49 | pre.output span.menu-dropdown { 50 | font-weight: normal; 51 | } 52 | 53 | pre.output div.menu-dropdown-content { 54 | font-family: Roboto, Helvetica, Arial, sans-serif; 55 | font-weight: normal; 56 | } 57 | 58 | pre.output .run-command-button { 59 | margin-bottom: 0; 60 | } 61 | 62 | pre.output .run-command-button:hover .menu-dropdown { 63 | background-color: #f9f9f9; 64 | color: black; 65 | } 66 | 67 | .dropdown { 68 | position: relative; 69 | display: inline-block; 70 | } 71 | 72 | .dropdown-content { 73 | display: none; 74 | position: absolute; 75 | background-color: #f9f9f9; 76 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 20%); 77 | z-index: 3; 78 | } 79 | 80 | .dropdown:hover .dropdown-content { 81 | display: block; 82 | } 83 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/events.css: -------------------------------------------------------------------------------- 1 | td.event-data { 2 | white-space: pre-wrap; 3 | } 4 | 5 | #page-events { 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/grains.css: -------------------------------------------------------------------------------- 1 | td.grain-value { 2 | white-space: pre-wrap; 3 | word-break: keep-all; 4 | } 5 | 6 | #page-grains { 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/job.css: -------------------------------------------------------------------------------- 1 | #page-job .time { 2 | font-size: 15px; 3 | font-weight: normal; 4 | margin-top: 5px; 5 | margin-bottom: 5px; 6 | } 7 | 8 | .warning { 9 | font-size: 15px; 10 | font-weight: normal; 11 | margin-top: 5px; 12 | margin-bottom: 5px; 13 | } 14 | 15 | #page-job .job-menu { 16 | display: inline; 17 | } 18 | 19 | .highlight-task { 20 | background-color: gray; 21 | } 22 | 23 | #summary-list-job { 24 | /* some room for the triangle (when shown) */ 25 | margin-right: 0.7em; 26 | } 27 | 28 | .triangle { 29 | /* some room for the triangle */ 30 | margin-left: 0.7em; 31 | margin-right: 0.7em; 32 | } 33 | 34 | .task-icon { 35 | /* some room for the marker */ 36 | margin-right: 0.7em; 37 | } 38 | 39 | .minion-output { 40 | margin-left: 0.7em; 41 | } 42 | 43 | .minion-output-single { 44 | display: inline; 45 | } 46 | 47 | .minion-output-multiple { 48 | display: block; 49 | } 50 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/keys.css: -------------------------------------------------------------------------------- 1 | #page-keys { 2 | width: 100%; 3 | } 4 | 5 | td.fingerprint { 6 | font-family: monospace; 7 | overflow: hidden; 8 | text-overflow: ellipsis; 9 | 10 | /* td forces a min-width, which wins */ 11 | max-width: 1px; 12 | } 13 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/login.css: -------------------------------------------------------------------------------- 1 | #page-login { 2 | display: flex; 3 | justify-content: center; 4 | width: 100%; 5 | position: absolute; 6 | height: 100%; 7 | top: 0; 8 | } 9 | 10 | @media not print { 11 | #page-login { 12 | background-color: #263238; 13 | } 14 | } 15 | 16 | #login-panel { 17 | align-self: center; 18 | background-color: white; 19 | box-shadow: 0 0 24px rgba(0, 0, 0, 70%); 20 | border-radius: 2px; 21 | 22 | /* 1px needed to prevent bottom margin to disappear when using small screens */ 23 | padding: 0 50px 1px; 24 | } 25 | 26 | #login-panel h1 { 27 | text-align: center; 28 | margin: 38px 0; 29 | margin-bottom: 23px; 30 | font-weight: lighter; 31 | font-size: 60px; 32 | width: 100%; 33 | color: #505050; 34 | } 35 | 36 | #login-panel input { 37 | width: 100%; 38 | display: block; 39 | font-size: 18px; 40 | } 41 | 42 | #login-panel select { 43 | width: 100%; 44 | font-size: 14px; 45 | } 46 | 47 | #login-panel select option#eauth-default { 48 | color: gray; 49 | } 50 | 51 | .attribution { 52 | display: block; 53 | opacity: 0.4; 54 | font-size: 15px; 55 | margin-top: 40px; 56 | margin-bottom: 20px; 57 | text-align: center; 58 | } 59 | 60 | .attribution img { 61 | width: 25px; 62 | } 63 | 64 | #notice { 65 | height: 0; 66 | overflow-y: hidden; 67 | color: white; 68 | padding: 0; 69 | border-radius: 2px; 70 | text-align: center; 71 | animation-name: show-notice; 72 | animation-iteration-count: 1; 73 | animation-duration: 5s; 74 | } 75 | 76 | @keyframes show-notice { 77 | 0% { 78 | height: 0; 79 | padding: 0; 80 | margin-bottom: 15px; 81 | } 82 | 83 | 20% { 84 | height: 38px; 85 | padding: 9px 0; 86 | margin-bottom: 15px; 87 | } 88 | 89 | 80% { 90 | height: 38px; 91 | padding: 9px 0; 92 | margin-bottom: 15px; 93 | } 94 | 95 | 100% { 96 | height: 0; 97 | padding: 0; 98 | margin-bottom: 0; 99 | } 100 | } 101 | 102 | #notice.notice-session-expired, 103 | #notice.notice-session-cancelled { 104 | /* keep these visible because user is likely not present when this happens */ 105 | animation-name: show-notice-stay; 106 | height: 38px; 107 | padding: 9px 0; 108 | margin-bottom: 15px; 109 | } 110 | 111 | @keyframes show-notice-stay { 112 | 0% { 113 | height: 0; 114 | padding: 0; 115 | } 116 | 117 | 20% { 118 | height: 38px; 119 | padding: 9px 0; 120 | } 121 | } 122 | 123 | .motd { 124 | margin-bottom: 15px; 125 | } 126 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | padding: 0; 6 | margin: 0; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | font-family: Roboto, Helvetica, Arial, sans-serif; 13 | } 14 | 15 | @media not print { 16 | body { 17 | background-color: #263238; 18 | } 19 | } 20 | 21 | header { 22 | border-top: 2px solid #4caf50; 23 | background-color: white; 24 | margin-top: 0; 25 | } 26 | 27 | .logo { 28 | cursor: pointer; 29 | color: #4caf50; 30 | font-size: 30px; 31 | font-weight: normal; 32 | display: inline-block; 33 | padding: 10px 20px; 34 | 35 | /* prevent text selection */ 36 | -webkit-user-select: none; 37 | -khtml-user-select: none; 38 | -moz-user-select: none; 39 | -o-user-select: none; 40 | user-select: none; 41 | } 42 | 43 | .docu { 44 | cursor: pointer; 45 | color: gray; 46 | font-size: 30px; 47 | font-weight: normal; 48 | display: inline-block; 49 | float: right; 50 | padding: 10px 20px; 51 | 52 | /* prevent text selection */ 53 | -webkit-user-select: none; 54 | -khtml-user-select: none; 55 | -moz-user-select: none; 56 | -o-user-select: none; 57 | user-select: none; 58 | } 59 | 60 | .docu:hover { 61 | color: #4caf50; 62 | } 63 | 64 | h1 { 65 | color: #4caf50; 66 | font-weight: lighter; 67 | font-size: 20px; 68 | margin: 0 10px 0 0; 69 | display: inline-block; 70 | } 71 | 72 | .msg { 73 | color: #505050; 74 | padding-top: 5px; 75 | } 76 | 77 | .panel { 78 | background-color: white; 79 | padding: 20px; 80 | border-radius: 1px; 81 | 82 | /* separate main-panel and jobs-panel */ 83 | margin: 5px 0 0 5px; 84 | } 85 | 86 | .panel:first-of-type { 87 | margin-left: 0; 88 | } 89 | 90 | @media (width < 950px) { 91 | .panel { 92 | margin-left: 0; 93 | } 94 | } 95 | 96 | .panel h1 { 97 | font-size: 20px; 98 | font-weight: normal; 99 | } 100 | 101 | .fab { 102 | display: inline-block; 103 | float: right; 104 | margin-top: 8px; 105 | margin-right: 8px; 106 | } 107 | 108 | #button-manual-run { 109 | opacity: 0.8; 110 | cursor: pointer; 111 | } 112 | 113 | .small-button { 114 | display: inline-block; 115 | min-width: 50px; 116 | text-align: center; 117 | background-color: #eee; 118 | margin: 0; 119 | cursor: pointer; 120 | font-size: 18px; 121 | color: #666; 122 | height: 24px; 123 | vertical-align: middle; 124 | padding-left: 10px; 125 | padding-right: 10px; 126 | } 127 | 128 | .smaller-small-button { 129 | padding-left: 5px; 130 | padding-right: 5px; 131 | min-width: 0; 132 | } 133 | 134 | .verysmall-small-button { 135 | padding-left: 5px; 136 | padding-right: 5px; 137 | min-width: 0; 138 | display: inherit; 139 | font-size: inherit; 140 | height: inherit; 141 | } 142 | 143 | .small-button-left { 144 | margin-right: 10px; 145 | } 146 | 147 | .small-button-right { 148 | margin-left: 10px; 149 | float: right; 150 | } 151 | 152 | .small-button:hover { 153 | color: #4caf50; 154 | } 155 | 156 | .small-button-for-hover { 157 | cursor: default; 158 | } 159 | 160 | .small-button-for-click { 161 | cursor: pointer; 162 | } 163 | 164 | .search-box { 165 | display: block; 166 | width: 100%; 167 | } 168 | 169 | .search-menu-and-field { 170 | margin-top: 15px; 171 | margin-bottom: 5px; 172 | display: flex; 173 | } 174 | 175 | .search-error { 176 | display: block; 177 | color: red; 178 | margin-bottom: 10px; 179 | margin-left: 10px; 180 | } 181 | 182 | .menu-item { 183 | display: inline-block; 184 | font-size: 18px; 185 | font-weight: lighter; 186 | padding: 15px 30px; 187 | } 188 | 189 | .menu-item-active { 190 | font-weight: bold; 191 | } 192 | 193 | .menu-item:hover { 194 | background: rgba(0, 0, 0, 15%); 195 | color: #4caf50; 196 | cursor: pointer; 197 | } 198 | 199 | .menu-item-first-letter#button-logout1:hover::first-letter, 200 | .menu-item-first-letter#button-logout2:hover::first-letter, 201 | .menu-item-first-letter#minimenu-top:hover::first-letter { 202 | text-decoration: none; 203 | } 204 | 205 | .menu-item-first-letter:hover::first-letter { 206 | text-decoration: underline #4caf50 double; 207 | text-decoration-skip-ink: none; 208 | } 209 | 210 | @media (width >= 950px) { 211 | .fullmenu { display: inline-block; } 212 | .minimenu { display: none; } 213 | } 214 | 215 | @media (width < 950px) { 216 | .fullmenu { display: none; } 217 | .minimenu { display: inline-block; } 218 | } 219 | 220 | #warning { 221 | background: yellow; 222 | margin-top: 5px; 223 | padding: 5px 10px 5px 20px; 224 | } 225 | 226 | .route { 227 | opacity: 0; 228 | pointer-events: none; 229 | transition: opacity 0.5s; /* Can be used to add transitions between views */ 230 | position: absolute; 231 | width: 100%; 232 | } 233 | 234 | .route.current { 235 | opacity: 1; 236 | pointer-events: all; 237 | z-index: 2; 238 | } 239 | 240 | .popup { 241 | width: 100%; 242 | height: 100%; 243 | z-index: 2; 244 | display: block; 245 | } 246 | 247 | .popup::before { 248 | position: fixed; 249 | display: block; 250 | content: ""; 251 | top: 0; 252 | left: 0; 253 | width: 100%; 254 | height: 100%; 255 | z-index: 2; 256 | background-color: rgba(0, 0, 0, 86%); 257 | } 258 | 259 | .popup h1 { 260 | font-weight: normal; 261 | font-size: 30px; 262 | margin: 0 10px 20px 0; 263 | } 264 | 265 | .run-command { 266 | padding: 30px; 267 | z-index: 3; 268 | background-color: white; 269 | position: relative; 270 | top: 5px; 271 | margin-left: 15px; 272 | margin-right: 15px; 273 | width: calc(100% - 30px); /* padding+margin */ 274 | } 275 | 276 | #run-command { 277 | margin-bottom: 20px; 278 | } 279 | 280 | pre.output { 281 | background-color: #272727; 282 | color: white; 283 | margin: 0; 284 | padding: 10px; 285 | border-radius: 2px; 286 | white-space: pre-wrap; 287 | } 288 | 289 | .run-command pre.output { 290 | min-height: 250px; 291 | } 292 | 293 | .search-box input { 294 | display: block; 295 | width: 100%; 296 | margin-bottom: 10px; 297 | flex: 1; 298 | } 299 | 300 | .warning-button { 301 | display: block; 302 | float: right; 303 | font-size: 18px; 304 | } 305 | 306 | .warning-button:hover { 307 | color: #4caf50; 308 | cursor: pointer; 309 | } 310 | 311 | .state-details-compressed { 312 | color: gray; 313 | } 314 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/options.css: -------------------------------------------------------------------------------- 1 | #options-table input { 2 | width: initial; 3 | min-width: initial; 4 | } 5 | 6 | label { 7 | margin-left: 5px; 8 | } 9 | 10 | #options-table label { 11 | margin-right: 20px; 12 | } 13 | 14 | #options-table tr td { 15 | white-space: pre-wrap; 16 | vertical-align: top; 17 | } 18 | 19 | #stats-table tr td { 20 | white-space: pre-wrap; 21 | } 22 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/page.css: -------------------------------------------------------------------------------- 1 | #page-minions { 2 | width: 100%; 3 | } 4 | 5 | .dashboard { 6 | display: flex; 7 | align-items: flex-start; 8 | } 9 | 10 | .dashboard .panel { 11 | width: 100%; 12 | } 13 | 14 | pre a.disabled { 15 | color: inherit; 16 | } 17 | 18 | pre a.disabled:hover { 19 | text-decoration: none; 20 | } 21 | 22 | pre.output a { 23 | color: yellow; 24 | cursor: pointer; 25 | } 26 | 27 | form a:link, 28 | pre a:link { 29 | text-decoration: none; 30 | } 31 | 32 | form a:hover, 33 | pre a:hover { 34 | text-decoration: underline; 35 | } 36 | 37 | pre.output div { 38 | margin-top: 10px; 39 | } 40 | 41 | pre.output div:first-of-type { 42 | margin-top: 0; 43 | } 44 | 45 | pre .minion-id.host-success, 46 | pre #summary-jobs-active .host-success { 47 | color: lime; 48 | } 49 | 50 | pre .minion-id.host-failure, 51 | pre #summary-jobs-active .host-failure { 52 | color: red; 53 | } 54 | 55 | td.address > span { 56 | color: #3f51b5; 57 | cursor: copy; 58 | position: relative; 59 | } 60 | 61 | td.tasks span { 62 | padding-top: 3px; 63 | padding-bottom: 3px; 64 | } 65 | 66 | td.tasks span.task:first-child, 67 | td.tasks span.tasksummary { 68 | padding-left: 2px; 69 | } 70 | 71 | td.tasks span.task:last-child, 72 | td.tasks span.tasksummary { 73 | padding-right: 2px; 74 | } 75 | 76 | pre .minion-id.host-skips, 77 | pre #summary-jobs-active .host-skips, 78 | pre .minion-id.host-no-response, 79 | pre #summary-jobs-active .host-no-response { 80 | color: yellow; 81 | } 82 | 83 | pre .minion-id { 84 | color: #4caf50; 85 | } 86 | 87 | .task-summary { 88 | word-break: break-all; 89 | line-height: 2em; 90 | } 91 | 92 | pre span.active { 93 | color: greenyellow; 94 | font-weight: bold; 95 | } 96 | 97 | table { 98 | min-width: 100%; 99 | border-spacing: 5px; 100 | border-collapse: collapse; 101 | } 102 | 103 | table tr th { 104 | border-bottom: 3px double #4caf50; 105 | padding-right: 20px; 106 | padding-top: 5px; 107 | padding-bottom: 5px; 108 | text-align: left; 109 | min-width: 20px; 110 | white-space: nowrap; 111 | } 112 | 113 | table tr td { 114 | padding-right: 20px; 115 | vertical-align: middle; 116 | padding-top: 5px; 117 | padding-bottom: 5px; 118 | white-space: nowrap; 119 | } 120 | 121 | .no-job-status { 122 | color: gray; 123 | } 124 | 125 | .no-job-details { 126 | color: gray; 127 | } 128 | 129 | table thead th, 130 | table tbody td { 131 | padding: 8px; 132 | text-align: left; 133 | border-bottom: 1px solid #ddd; 134 | color: #505050; 135 | } 136 | 137 | table thead th { 138 | border-bottom: 2px solid #ddd; 139 | } 140 | 141 | table tr th:last-child { 142 | padding-right: 0; 143 | } 144 | 145 | table tr td:last-child { 146 | width: 100%; 147 | } 148 | 149 | table tr:last-of-type td { 150 | border-bottom: none; 151 | } 152 | 153 | table tr td:last-of-type { 154 | padding-right: 0; 155 | } 156 | 157 | .value-none { 158 | opacity: 0.4; 159 | } 160 | 161 | .menu-item-hidden { 162 | display: none; 163 | } 164 | 165 | .run-command-button { 166 | color: #263238; 167 | cursor: pointer; 168 | } 169 | 170 | .run-command-button:hover { 171 | color: #2e7d32; 172 | } 173 | 174 | #template-catmenu-here, 175 | #template-tmplmenu-here { 176 | display: inline; 177 | } 178 | 179 | .jobs td.details > span, 180 | .jobs td.job-status > span { 181 | position: relative; 182 | } 183 | 184 | .accepted { 185 | color: #00a000; 186 | } 187 | 188 | .denied { 189 | color: #f0f; 190 | } 191 | 192 | .unaccepted, 193 | .keyunknown { 194 | color: #f00; 195 | } 196 | 197 | .rejected { 198 | color: #00f; 199 | } 200 | 201 | .offline { 202 | color: red; 203 | } 204 | 205 | .prefiximage { 206 | max-width: 18px; 207 | max-height: 18px; 208 | padding-right: 5px; 209 | box-sizing: content-box; 210 | vertical-align: middle; 211 | } 212 | 213 | #popup-run-command { 214 | display: none; 215 | } 216 | 217 | .jobs td .target { 218 | font-weight: 500; 219 | font-size: 18px; 220 | color: #505050; 221 | white-space: nowrap; 222 | } 223 | 224 | .jobs td .function { 225 | font-weight: 500; 226 | font-size: 14px; 227 | color: #3a3a3a; 228 | } 229 | 230 | .jobs td .time { 231 | font-size: 11px; 232 | } 233 | 234 | .job-status { 235 | font-size: 12px; 236 | } 237 | 238 | #page-jobs .job-status { 239 | font-size: inherit; 240 | } 241 | 242 | #jobs-panel { 243 | /* the left panel has the default 100%, therefore the ratio is 2/3 vs 1/3 */ 244 | flex-basis: 50%; 245 | } 246 | 247 | #page-jobs #jobs-panel { 248 | /* no right panel here */ 249 | flex-basis: 100%; 250 | } 251 | 252 | .highlight-rows tbody tr:hover { 253 | background-color: whitesmoke; 254 | cursor: pointer; 255 | } 256 | 257 | .no-filter-match { 258 | display: none; 259 | } 260 | 261 | .filter-text { 262 | flex-grow: 1; 263 | } 264 | 265 | @media (width < 950px) { 266 | .minions { 267 | min-width: 0; 268 | } 269 | 270 | .dashboard { 271 | display: block; 272 | } 273 | 274 | .dashboard .panel { 275 | width: auto; 276 | } 277 | } 278 | 279 | /* tasks */ 280 | 281 | .task-success { 282 | color: lime; 283 | } 284 | 285 | .task-success-changes { 286 | color: aqua; 287 | } 288 | 289 | .task-failure, 290 | .task-failure-changes { 291 | color: red; 292 | } 293 | 294 | .task-skipped, 295 | .task-skipped-changes { 296 | color: yellow; 297 | } 298 | 299 | pre .task-success, 300 | pre .task-success-changes, 301 | pre .task-skipped, 302 | pre .task-skipped-changes, 303 | pre .task-failure, 304 | pre .task-failure-changes { 305 | cursor: pointer; 306 | } 307 | 308 | @media print { 309 | .no-print, 310 | .no-print * { 311 | display: none !important; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/pillars.css: -------------------------------------------------------------------------------- 1 | #page-pillars { 2 | width: 100%; 3 | } 4 | 5 | .pillars { 6 | padding: 0; 7 | } 8 | 9 | .pillar-hidden { 10 | cursor: pointer; 11 | } 12 | 13 | .pillar-shown { 14 | cursor: pointer; 15 | white-space: pre; 16 | } 17 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/schedules.css: -------------------------------------------------------------------------------- 1 | #page-schedules { 2 | width: 100%; 3 | } 4 | 5 | td.schedule-value { 6 | white-space: pre-wrap; 7 | } 8 | 9 | td.schedule-disabled { 10 | color: gray; 11 | } 12 | -------------------------------------------------------------------------------- /saltgui/static/stylesheets/tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: relative; 3 | } 4 | 5 | .tooltip > .tooltip-text { 6 | display: none; 7 | font-size: 14px; 8 | background-color: rgba(76, 175, 80, 80%); /* #4caf50 */ 9 | color: white; 10 | padding: 7px; 11 | border-radius: 3px; 12 | position: absolute; 13 | white-space: pre; 14 | font-weight: normal; 15 | line-height: initial; 16 | 17 | /* point the tooltip to its target */ 18 | left: 50%; 19 | 20 | /* float the tooltip above its target */ 21 | z-index: 10; 22 | bottom: calc(100% + 8px); 23 | 24 | /* shorten the text when it is too much 25 | do not clip, as it also clips the arrow 26 | overflow: hidden; */ 27 | max-width: 500px; 28 | text-overflow: ellipsis; 29 | } 30 | 31 | .tooltip > .tooltip-text-bottom-left { 32 | text-align: left; 33 | transform: translate(-5%, 0); 34 | } 35 | 36 | .tooltip > .tooltip-text-error-bottom-left { 37 | text-align: left; 38 | transform: translate(-5%, 0); 39 | background-color: red; 40 | font-weight: bold; 41 | } 42 | 43 | .tooltip > .tooltip-text-bottom-center { 44 | text-align: center; 45 | transform: translate(-50%, 0); 46 | } 47 | 48 | .tooltip > .tooltip-text-bottom-right { 49 | text-align: right; 50 | transform: translate(-95%, 0); 51 | } 52 | 53 | .tooltip > .tooltip-text-fab { 54 | text-align: right; 55 | transform: translate(-163px, 52px); 56 | } 57 | 58 | .tooltip:hover { 59 | /* only slightly darker than 'whitesmoke(#f5f5f5)' */ 60 | background-color: #e0e0e0; 61 | } 62 | 63 | .tooltip:hover > .tooltip-text { 64 | display: initial; 65 | } 66 | 67 | pre.output .tooltip > .tooltip-text { 68 | background-color: rgba(76, 175, 80, 80%); /* #4caf50 */ 69 | } 70 | 71 | pre.output .tooltip:hover { 72 | /* only slightly lighter than '#272727' */ 73 | background-color: #484848; 74 | } 75 | 76 | /* The arrow/triangle of the tooltip */ 77 | 78 | .tooltip > .tooltip-text::after { 79 | font-size: 8px; 80 | content: " "; 81 | position: absolute; 82 | margin-left: -5px; 83 | border-width: 5px; 84 | border-style: solid; 85 | top: 100%; 86 | border-color: rgba(76, 175, 80, 80%) transparent transparent transparent; 87 | } 88 | 89 | .tooltip > .tooltip-text-logo { 90 | left: 110px; 91 | top: -5px; 92 | height: calc(150% - 5px); 93 | } 94 | 95 | .tooltip > .tooltip-text-logo::after { 96 | /* hide the tooltip pointer */ 97 | display: none; 98 | } 99 | 100 | .tooltip > .tooltip-text-fab::after { 101 | /* hide the tooltip pointer */ 102 | display: none; 103 | } 104 | 105 | .tooltip > .tooltip-text-bottom-left::after { 106 | left: calc(5% - 2.5px); 107 | } 108 | 109 | .tooltip > .tooltip-text-error-bottom-left::after { 110 | left: calc(5% - 2.5px); 111 | border-color: red transparent transparent; 112 | } 113 | 114 | .tooltip > .tooltip-text-bottom-center::after { 115 | left: calc(50% - 2.5px); 116 | } 117 | 118 | .tooltip > .tooltip-text-bottom-right::after { 119 | left: calc(95% - 2.5px); 120 | } 121 | 122 | pre.output .tooltip > .tooltip-text::after { 123 | border-color: rgba(76, 175, 80, 80%) transparent transparent transparent; /* #4caf50 */ 124 | } 125 | -------------------------------------------------------------------------------- /tests/functional/login.js: -------------------------------------------------------------------------------- 1 | /* global afterEach beforeEach describe it process */ 2 | 3 | import Nightmare from "nightmare"; 4 | import {assert} from "chai"; 5 | 6 | const url = "http://localhost:3333/"; 7 | 8 | /* eslint-disable func-names */ 9 | describe("Funtional tests", function () { 10 | /* eslint-enable func-names */ 11 | 12 | let browser = null; 13 | 14 | // the global electron timeout 15 | /* eslint-disable no-invalid-this */ 16 | this.timeout(60 * 1000); 17 | /* eslint-enable no-invalid-this */ 18 | 19 | beforeEach(() => { 20 | const options = { 21 | "fullscreen": true, 22 | // to make the typed input much faster 23 | "typeInterval": 20, 24 | // the wait function has a timeout as well 25 | "waitTimeout": 60 * 1000 26 | }; 27 | 28 | if (process.env.NIGHTMARE_DEBUG === "1") { 29 | console.log("NIGHTMARE_DEBUG=1, setting additional options"); 30 | 31 | // show the browser and the debug window 32 | options.openDevTools = true; 33 | // to show in a separate window 34 | // options.openDevTools = { mode: "detach" }; 35 | } 36 | 37 | browser = new Nightmare(options); 38 | 39 | browser.on('console', (type, message) => { 40 | console.log(`[console][${type}] ` + JSON.stringify(message, null, 2)); 41 | }); 42 | 43 | browser.on('page', (type, message, stack) => { 44 | console.error(`[page-error][${type}] ${JSON.stringify(message)}`); 45 | if (stack) { 46 | console.error('stack:', stack); 47 | } 48 | }); 49 | 50 | return browser. 51 | goto(url). 52 | wait(1000); 53 | }); 54 | 55 | /* eslint-disable arrow-body-style */ 56 | afterEach(() => { 57 | return browser.end(); 58 | }); 59 | /* eslint-enable arrow-body-style */ 60 | 61 | describe("Login and logout", () => { 62 | 63 | it("we should be redirected to the login page", (done) => { 64 | browser. 65 | wait(() => document.location.href.includes("login")). 66 | wait(500). 67 | evaluate(() => document.location.href). 68 | then((href) => { 69 | href = href.replace(/[?]reason=.*/, ""); 70 | assert.equal(href, url); 71 | }). 72 | then(done). 73 | catch((err) => done(err)); 74 | }); 75 | 76 | it("we cannot login with false credentials", (done) => { 77 | browser. 78 | insert("#username", "sald"). 79 | wait(500). 80 | insert("#password", "sald"). 81 | wait(500). 82 | click("#login-button"). 83 | wait(500). 84 | wait("#notice-wrapper div.notice_auth_failed"). 85 | wait(1000). 86 | evaluate(() => document.querySelector("#notice-wrapper div").textContent). 87 | then((message) => { 88 | assert.equal(message, "Authentication failed"); 89 | }). 90 | then(done). 91 | catch(done); 92 | }); 93 | 94 | it("valid credentials will redirect us to the homepage and hide the loginform", (done) => { 95 | browser. 96 | insert("#username", "salt"). 97 | wait(500). 98 | insert("#password", "salt"). 99 | wait(500). 100 | click("#login-button"). 101 | wait(500). 102 | wait(() => { 103 | // we wait here for the loginpage to be hidden 104 | const loginpage = document.querySelector("#page-login"); 105 | return loginpage.style.display === "none"; 106 | }). 107 | wait(1000). 108 | evaluate(() => document.location.href). 109 | then((href) => { 110 | assert.equal(href, url + "#minions"); 111 | }). 112 | then(done). 113 | catch(done); 114 | }); 115 | 116 | it("check that we can logout", (done) => { 117 | browser. 118 | insert("#username", "salt"). 119 | wait(500). 120 | insert("#password", "salt"). 121 | wait(500). 122 | click("#login-button"). 123 | wait(500). 124 | wait("#notice-wrapper div.notice_please_wait"). 125 | wait(5000). 126 | wait(() => { 127 | // we wait here for the loginpage to be hidden 128 | const loginpage = document.querySelector("#page-login"); 129 | return loginpage.style.display === "none"; 130 | }). 131 | click("#button-logout1"). 132 | wait(500). 133 | wait(() => { 134 | // we wait here for the loginpage to be shown 135 | const loginpage = document.querySelector("#page-login"); 136 | return loginpage.style.display === ""; 137 | }). 138 | wait(() => document.location.href.includes("login")). 139 | wait(1000). 140 | evaluate(() => document.location.href). 141 | then((href) => { 142 | // and we redirected to the login page 143 | assert.equal(href, url + "?reason=logout#login"); 144 | }). 145 | then(done). 146 | catch(done); 147 | }); 148 | 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /tests/helpers/wait-for-docker.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import request from "request"; 4 | 5 | const url = "http://localhost:3333"; 6 | 7 | console.log("waiting for docker setup to be ready"); 8 | 9 | const waitfordocker = () => { 10 | request. 11 | get(url). 12 | on("response", () => { 13 | console.log("docker setup is ready"); 14 | }). 15 | on("error", (err) => { 16 | console.log("docker setup is NOT ready yet:", err.code); 17 | setTimeout(waitfordocker, 1000); 18 | }); 19 | }; 20 | 21 | waitfordocker(); 22 | -------------------------------------------------------------------------------- /tests/unit/TargetType.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | 3 | import {TargetType} from "../../saltgui/static/scripts/TargetType.js"; 4 | import {assert} from "chai"; 5 | 6 | /* eslint-disable func-names */ 7 | const testTargetType = function (targetType, targetPattern) { 8 | const obj = {}; 9 | obj["Target-type"] = targetType; 10 | obj.Target = targetPattern; 11 | return TargetType.makeTargetText(obj); 12 | }; 13 | /* eslint-enable func-names */ 14 | 15 | describe("Unittests for TargetType.js", () => { 16 | 17 | it("test makeTargetText", (done) => { 18 | 19 | let result; 20 | 21 | // list of target-types from: 22 | // https://docs.saltstack.com/en/latest/ref/clients/index.html#salt.client.LocalClient.cmd 23 | 24 | // glob - Bash glob completion - Default 25 | result = testTargetType("glob", "*"); 26 | assert.equal(result, "*"); 27 | 28 | // pcre - Perl style regular expression 29 | result = testTargetType("pcre", ".*"); 30 | assert.equal(result, "pcre .*"); 31 | 32 | // list - Python list of hosts 33 | result = testTargetType("list", "a,b,c"); 34 | assert.equal(result, "a,b,c"); 35 | 36 | // grain - Match based on a grain comparison 37 | result = testTargetType("grain", "os:*"); 38 | assert.equal(result, "grain os:*"); 39 | 40 | // grain_pcre - Grain comparison with a regex 41 | result = testTargetType("grain_pcre", "os:.*"); 42 | assert.equal(result, "grain_pcre os:.*"); 43 | 44 | // pillar - Pillar data comparison 45 | result = testTargetType("pillar", "p1:*"); 46 | assert.equal(result, "pillar p1:*"); 47 | 48 | // pillar_pcre - Pillar data comparison with a regex 49 | result = testTargetType("pillar_pcre", "p1:.*"); 50 | assert.equal(result, "pillar_pcre p1:.*"); 51 | 52 | // nodegroup - Match on nodegroup 53 | result = testTargetType("nodegroup", "ng3"); 54 | assert.equal(result, "nodegroup ng3"); 55 | 56 | // range - Use a Range server for matching 57 | result = testTargetType("range", "a-z"); 58 | assert.equal(result, "range a-z"); 59 | 60 | // compound - Pass a compound match string 61 | result = testTargetType("compound", "webserv* and G@os:Debian or E@web-dc1-srv.*"); 62 | assert.equal(result, "compound webserv* and G@os:Debian or E@web-dc1-srv.*"); 63 | 64 | // ipcidr - Match based on Subnet (CIDR notation) or IPv4 address. 65 | result = testTargetType("ipcidr", "10.0.0.0/24"); 66 | assert.equal(result, "ipcidr 10.0.0.0/24"); 67 | 68 | done(); 69 | }); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /tests/unit/Utils.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | 3 | import {Utils} from "../../saltgui/static/scripts/Utils.js"; 4 | import {assert} from "chai"; 5 | 6 | describe("Unittests for Utils.js", () => { 7 | 8 | it("test getQueryParam2", (done) => { 9 | let result; 10 | 11 | // no parameters 12 | result = Utils._getQueryParam2("http://host/url", "aap"); 13 | assert.equal(result, undefined); 14 | 15 | // no parameters 16 | result = Utils._getQueryParam2("http://host/url?", "aap"); 17 | assert.equal(result, undefined); 18 | 19 | // one parameter, match 20 | result = Utils._getQueryParam2("http://host/url?aap=1", "aap"); 21 | assert.equal(result, "1"); 22 | 23 | // one parameter, no match 24 | result = Utils._getQueryParam2("http://host/url?aap=1", "noot"); 25 | assert.equal(result, undefined); 26 | 27 | // one parameter, illegal format 28 | result = Utils._getQueryParam2("http://host/url?aap", "aap"); 29 | assert.equal(result, undefined); 30 | 31 | // one parameter, illegal format 32 | result = Utils._getQueryParam2("http://host/url?aap=1=2", "aap"); 33 | assert.equal(result, undefined); 34 | 35 | // more parameters, match 36 | result = Utils._getQueryParam2("http://host/url?aap=1&noot=2", "aap"); 37 | assert.equal(result, "1"); 38 | 39 | // more parameters, match 40 | result = Utils._getQueryParam2("http://host/url?aap=1&noot=2", "noot"); 41 | assert.equal(result, "2"); 42 | 43 | // more parameters, no match 44 | result = Utils._getQueryParam2("http://host/url?aap=1&noot=2", "mies"); 45 | assert.equal(result, undefined); 46 | 47 | // mark function as used 48 | // it has implicit parameter windows.location which we will not fake 49 | result = Utils.getQueryParam("lkhlkfhlaskdhfljk"); 50 | assert.equal(result, undefined); 51 | 52 | done(); 53 | }); 54 | 55 | }); 56 | --------------------------------------------------------------------------------