├── .dockerignore ├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── compose.yml ├── crontab ├── elm-client ├── Components.elm ├── Components │ ├── NetworkConnections.elm │ └── Networks.elm ├── Docker.elm ├── Docker │ ├── Json.elm │ └── Types.elm ├── Main.elm ├── Util.elm ├── client │ ├── docker_logo.svg │ └── index.html └── elm-package.json ├── healthcheck.sh ├── package.json ├── server.sh ├── server └── index.js ├── swarm.gif ├── test-cluster ├── .gitignore ├── Vagrantfile ├── commands.md ├── compose-all.yml ├── compose-dashboard.yml ├── compose-metrics.yml └── wait-for-docker.sh └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | elm-stuff 3 | npm-debug.log 4 | Dockerfile 5 | rebuild.sh 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | "rules": { 9 | "consistent-return": "error", 10 | "no-console": "warn" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # config from https://github.com/docker/metadata-action 2 | 3 | name: Publish Docker image 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - 'master' 10 | - 'dev_*' 11 | - 'fix_*' 12 | tags: 13 | - 'v*' 14 | 15 | jobs: 16 | docker: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - 20 | name: Checkout 21 | uses: actions/checkout@v4 22 | - 23 | name: Docker meta 24 | id: meta 25 | uses: docker/metadata-action@v5 26 | with: 27 | images: mohsenasm/swarm-dashboard 28 | - 29 | name: Set up QEMU 30 | uses: docker/setup-qemu-action@v3 31 | - 32 | name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v3 34 | - 35 | name: Login to DockerHub 36 | if: github.event_name != 'pull_request' 37 | uses: docker/login-action@v3 38 | with: 39 | username: mohsenasm 40 | password: ${{ secrets.DOCKERHUB_TOKEN }} 41 | - 42 | name: Build and push 43 | uses: docker/build-push-action@v5 44 | with: 45 | context: . 46 | platforms: linux/amd64,linux/arm64/v8,linux/ppc64le 47 | push: ${{ github.event_name != 'pull_request' }} 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | # yarn install can be very slow in some arm platforms, because of QEMU. 51 | # https://github.com/nodejs/docker-node/issues/1335 52 | # - 53 | # name: Build and push 54 | # uses: docker/build-push-action@v5 55 | # with: 56 | # context: . 57 | # platforms: linux/amd64,linux/arm64/v8,linux/ppc64le,linux/arm/v7,linux/arm/v6 58 | # push: ${{ github.event_name != 'pull_request' }} 59 | # tags: ${{ steps.meta.outputs.tags }} 60 | # labels: ${{ steps.meta.outputs.labels }} 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | elm-stuff 3 | npm-debug.log 4 | sample-data 5 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS base 2 | RUN apk add --no-cache --update tini lego curl 3 | ENTRYPOINT ["/sbin/tini", "--"] 4 | WORKDIR /home/node/app 5 | 6 | FROM base AS dependencies 7 | ENV NODE_ENV production 8 | COPY package.json yarn.lock ./ 9 | RUN yarn install --production 10 | 11 | FROM --platform=linux/amd64 node:10.16.0-buster-slim AS elm-build 12 | RUN npm install --unsafe-perm -g elm@latest-0.18.0 --silent 13 | RUN apt-get -qq update && apt-get install -y netbase && rm -rf /var/lib/apt/lists/* 14 | WORKDIR /home/node/app/elm-client 15 | COPY ./elm-client/elm-package.json . 16 | RUN elm package install -y 17 | COPY ./elm-client/ /home/node/app/elm-client/ 18 | RUN elm make Main.elm --output=client/index.js 19 | 20 | FROM base AS release 21 | ENV LEGO_PATH=/lego-files 22 | COPY --from=dependencies /home/node/app/node_modules node_modules 23 | COPY --from=elm-build /home/node/app/elm-client/client/ client 24 | COPY package.json package.json 25 | COPY server server 26 | COPY server.sh server.sh 27 | COPY healthcheck.sh healthcheck.sh 28 | COPY crontab /var/spool/cron/crontabs/root 29 | 30 | ENV PORT=8080 31 | # HEALTHCHECK --interval=60s --timeout=30s \ 32 | # CMD sh healthcheck.sh 33 | 34 | # Run under Tini 35 | CMD ["sh", "server.sh"] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Viktor Charypar 4 | Copyright (c) 2024 Mohammad-Mohsen Aseman-Manzar 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swarm Dashboard 2 | 3 | A simple monitoring dashboard for Docker in Swarm Mode. 4 | 5 | [![Publish Docker image](https://github.com/mohsenasm/swarm-dashboard/actions/workflows/main.yml/badge.svg)](https://github.com/mohsenasm/swarm-dashboard/actions/workflows/main.yml) 6 | 7 | ![Example Dashboard](./swarm.gif) 8 | 9 | Swarm Dashboard shows you all the tasks running on a Docker Swarm organized 10 | by service and node. It provides a space-efficient visualization 11 | and works well at a glance. You can use it as a simple live dashboard of the state of your Swarm. 12 | 13 | It also shows the CPU/Memory/Disk usage of your swarm node and containers. 14 | 15 | ## Usage 16 | 17 | The dashboard needs to be deployed on one of the swarm managers. 18 | You can configure it with the following Docker compose file: 19 | 20 | ```yml 21 | # compose.yml 22 | version: "3" 23 | 24 | services: 25 | swarm-dashboard: 26 | image: mohsenasm/swarm-dashboard:latest 27 | volumes: 28 | - /var/run/docker.sock:/var/run/docker.sock 29 | ports: 30 | - 8080:8080 31 | environment: 32 | TZ: "your_timezone" 33 | ENABLE_AUTHENTICATION: "false" 34 | ENABLE_HTTPS: "false" 35 | NODE_EXPORTER_SERVICE_NAME_REGEX: "node-exporter" 36 | CADVISOR_SERVICE_NAME_REGEX: "cadvisor" 37 | deploy: 38 | placement: 39 | constraints: 40 | - node.role == manager 41 | 42 | node-exporter: 43 | image: quay.io/prometheus/node-exporter:v1.6.1 44 | volumes: 45 | - '/:/host:ro' 46 | command: 47 | - '--path.rootfs=/host' 48 | deploy: 49 | mode: global 50 | 51 | cadvisor: 52 | image: gcr.io/cadvisor/cadvisor:v0.47.2 53 | volumes: 54 | - /:/rootfs:ro 55 | - /var/run:/var/run:rw 56 | - /sys:/sys:ro 57 | - /var/lib/docker/:/var/lib/docker:ro 58 | - /dev/disk/:/dev/disk:ro 59 | deploy: 60 | mode: global 61 | ``` 62 | 63 | and deploy with 64 | 65 | ``` 66 | $ docker stack deploy -c compose.yml sd 67 | ``` 68 | 69 | Note that the usage of `node-exporter` and `cadvisor` are optional, to fetch node CPU/Memory/Disk usage and containers' CPU/Memory usage respectively. If you don't specify `NODE_EXPORTER_SERVICE_NAME_REGEX` and `CADVISOR_SERVICE_NAME_REGEX` envs, the default is not using this feature, because of backward compatibility. 70 | 71 | ## Advance Usage 72 | 73 | List of environment variables for more customization: 74 | 75 | | Enviroment Varibles | Example | Considration | 76 | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 77 | | PORT | 8080 | HTTP / HTTPS port. | 78 | | PATH_PREFIX | /prefix_path | All HTTP and WebSocket connections will use this path as a prefix. | 79 | | TZ | Asia/Tehran | Set the timezone for the time reported in the dashboard. | 80 | | SHOW_TASK_TIMESTAMP | false | `true` by default. | 81 | | ENABLE_AUTHENTICATION | true | `false` by default. | 82 | | AUTHENTICATION_REALM | MyRealm | Use this env if ENABLE_AUTHENTICATION is `true`. | 83 | | USERNAME | admin | Use this env if ENABLE_AUTHENTICATION is `true`. | 84 | | USERNAME_FILE | /run/secrets/username | Alternative to `USERNAME`. | 85 | | PASSWORD | supersecret | Use this env if ENABLE_AUTHENTICATION is `true`. | 86 | | PASSWORD_FILE | /run/secrets/password | Alternative to `PASSWORD`. | 87 | | ENABLE_HTTPS | true | `false` by default. | 88 | | LEGO_PATH | /lego-files | Use this env if ENABLE_HTTPS is `true`. Lego is used to create the SSL certificates. Create a named volume for this path to avoid the creation of a new certificate on each run. | 89 | | HTTPS_HOSTNAME | swarm-dashboard.example.com | Use this env if ENABLE_HTTPS is `true`. | 90 | | LEGO_NEW_COMMAND_ARGS | --accept-tos --email=you@swarm-dashboard.example.com --domains=swarm-dashboard.example.com --dns cloudflare run | Use this env if ENABLE_HTTPS is `true`. | 91 | | LEGO_RENEW_COMMAND_ARGS | --accept-tos --email=you@swarm-dashboard.example.com --domains=swarm-dashboard.example.com --dns cloudflare renew | Use this env if ENABLE_HTTPS is `true`. | 92 | | USE_RENEW_DELAY_ON_START | false | Lego usually adds a [small random delay](https://github.com/go-acme/lego/issues/1656) to the `renew` command, but we don't need this delay at the start because it's not an automated task. | 93 | | CLOUDFLARE_EMAIL | you@example.com | You can use any [DNS provider that Lego supports](https://go-acme.github.io/lego/dns/). | 94 | | CLOUDFLARE_API_KEY | yourprivatecloudflareapikey | You can use any [DNS provider that Lego supports](https://go-acme.github.io/lego/dns/). | 95 | | DOCKER_UPDATE_INTERVAL | 5000 | Refresh interval in ms. Choosing a low refresh interval will increase CPU load as it refreshes more frequently. | 96 | | METRICS_UPDATE_INTERVAL | 60000 | Refresh interval in ms. Choosing a low refresh interval will increase CPU load as it refreshes more frequently. | 97 | | NODE_EXPORTER_SERVICE_NAME_REGEX | node-exporter | Use this env to enable `node-exporter` integration. | 98 | | NODE_EXPORTER_INTERESTED_MOUNT_POINT | /rootfs | You may need this config if you have not specified `--path.rootfs` for `node-exporter`. | 99 | | NODE_EXPORTER_PORT | 9100 | | 100 | | CADVISOR_SERVICE_NAME_REGEX | cadvisor | Use this env to enable `cadvisor` integration. | 101 | | CADVISOR_PORT | 8080 | | 102 | | ENABLE_DATA_API | true | Use this env to export the `/data` API that returns the swarm status as a JSON object. Note that it requires basic-auth if `ENABLE_AUTHENTICATION` is activated. | 103 | | ENABLE_NETWORKS | false | `true` by default, set to `false` to remove the network section from the dashboard. | 104 | 105 | ## Security 106 | 107 | + We redact docker event data before sending them to the client. The previous version was sending the whole docker event data, including environment variables (someone might have stored some passwords in them, by mistake!). So, please consider using the newer version. 108 | 109 | + Using the `ENABLE_AUTHENTICATION` environment variable, there is an option to use `Basic Auth`. The WebSocket server will close the connection if it does not receive a valid authentication token. See the example in the above section for more info. 110 | 111 | + Using the `ENABLE_HTTPS` environment variable, there is an option to use `HTTPS` and `WSS`. We have Let’s Encrypt integration with the DNS challenge. See the example in the above section for more info. 112 | 113 | 114 | ## Production use 115 | 116 | There are two considerations for any serious deployment of the dashboard: 117 | 118 | 1. Security - the dashboard node.js server has access to the docker daemon unix socket 119 | and runs on the manager, which makes it a significant attack surface (i.e. compromising 120 | the dashboard's node server would give an attacker full control of the swarm) 121 | 2. The interaction with docker API is a fairly rough implementation and 122 | is not very optimized. The server polls the API every 1000 ms, publishing the 123 | response data to all open WebSockets if it changed since last time. There 124 | is probably a better way to look for changes in the Swarm that could be used 125 | in the future. 126 | 127 | 128 | ## Rough roadmap 129 | 130 | * Show more service details (published port, image name, and version) 131 | * Node / Service / Task details panel 132 | 133 | Both feature requests and pull requests are welcome. If you want to build/test the code locally, see [commands.md](./test-cluster/commands.md) in the `test-cluster` directory. 134 | 135 | ### Prior art 136 | 137 | * Heavily inspired by [Docker Swarm Visualiser](https://github.com/dockersamples/docker-swarm-visualizer) 138 | 139 | ## Contributors 140 | 141 | * Mohammad-Mohsen Aseman-Manzar (current maintainer) - code, docs 142 | * Viktor Charypar (previous repo owner) - code, docs 143 | * Clementine Brown - design 144 | 145 | ## Star History 146 | [![Stargazers over time](https://starchart.cc/mohsenasm/swarm-dashboard.svg?variant=adaptive)](https://starchart.cc/mohsenasm/swarm-dashboard) 147 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | dashboard: 5 | build: . 6 | image: localhost:5000/dashboard 7 | volumes: 8 | - "/var/run/docker.sock:/var/run/docker.sock" 9 | - lego-files:/lego-files 10 | ports: 11 | - 8081:8081 12 | environment: 13 | PORT: 8081 14 | TZ: "Asia/Tehran" 15 | # PATH_PREFIX: "/" 16 | # SHOW_TASK_TIMESTAMP: "false" 17 | # DOCKER_UPDATE_INTERVAL: 4000 18 | # ENABLE_AUTHENTICATION: "false" 19 | # ENABLE_AUTHENTICATION: "true" 20 | # AUTHENTICATION_REALM: "KuW2i9GdLIkql" 21 | # USERNAME: "admin" 22 | # USERNAME_FILE: "/run/secrets/username" 23 | # PASSWORD: "supersecret" 24 | # PASSWORD_FILE: "/run/secrets/password" 25 | # ENABLE_HTTPS: "false" 26 | # ENABLE_HTTPS: "true" 27 | # HTTPS_HOSTNAME: "example.com" 28 | # LEGO_NEW_COMMAND_ARGS: "--accept-tos --email=you@example.com --domains=example.com --dns cloudflare run" 29 | # LEGO_RENEW_COMMAND_ARGS: "--accept-tos --email=you@example.com --domains=example.com --dns cloudflare renew" 30 | # CLOUDFLARE_EMAIL: "you@example.com" 31 | # CLOUDFLARE_API_KEY: "yourprivatecloudflareapikey" 32 | # NODE_EXPORTER_SERVICE_NAME_REGEX: "node-exporter" 33 | # USE_RENEW_DELAY_ON_START: "false" 34 | deploy: 35 | replicas: 1 36 | update_config: 37 | parallelism: 1 38 | restart_policy: 39 | condition: on-failure 40 | placement: 41 | constraints: 42 | - node.role == manager 43 | 44 | volumes: 45 | lego-files: 46 | -------------------------------------------------------------------------------- /crontab: -------------------------------------------------------------------------------- 1 | # do daily/weekly/monthly maintenance 2 | # min hour day month weekday command 3 | */15 * * * * run-parts /etc/periodic/15min 4 | 0 * * * * run-parts /etc/periodic/hourly 5 | 0 2 * * * run-parts /etc/periodic/daily 6 | 0 3 * * 6 run-parts /etc/periodic/weekly 7 | 0 5 1 * * run-parts /etc/periodic/monthly 8 | */15 * * * * run-parts /etc/periodic/15min 9 | 17 2 1 * * /usr/local/bin/lego --path $LEGO_PATH $LEGO_RENEW_COMMAND_ARGS 10 | -------------------------------------------------------------------------------- /elm-client/Components.elm: -------------------------------------------------------------------------------- 1 | module Components exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Util exposing (..) 7 | import Docker.Types exposing (..) 8 | import Components.Networks as Networks 9 | 10 | 11 | statusString : String -> String -> String 12 | statusString state desiredState = 13 | if state == desiredState then 14 | state 15 | else 16 | state ++ " → " ++ desiredState 17 | 18 | 19 | task : Service -> AssignedTask -> Html msg 20 | task service { status, desiredState, containerSpec, slot, info } = 21 | let 22 | classes = 23 | [ ( status.state, True ) 24 | , ( "task", True ) 25 | , ( "desired-" ++ desiredState, True ) 26 | , ( "running-old", status.state == "running" && service.containerSpec.image /= containerSpec.image ) 27 | ] 28 | 29 | slotLabel slot = 30 | case slot of 31 | Just s -> 32 | "." ++ toString s 33 | 34 | Nothing -> 35 | "" 36 | 37 | cpuInfo = 38 | case info.cpu of 39 | Just s -> 40 | [ 41 | div [ class "tag left" ] [ text s ] 42 | ] 43 | 44 | Nothing -> 45 | [] 46 | 47 | memoryInfo = 48 | case info.memory of 49 | Just s -> 50 | [ 51 | div [ class "tag right" ] [ text s ] 52 | ] 53 | 54 | Nothing -> 55 | [] 56 | 57 | timestateInfo = 58 | case status.timestateInfo of 59 | Just s -> 60 | [ small [] [ text ( " (" ++ s ++ ")") ] ] 61 | 62 | Nothing -> 63 | [] 64 | in 65 | li [ classList classes ] 66 | (List.concat [ 67 | cpuInfo 68 | , (List.concat [ 69 | memoryInfo 70 | , (List.concat [ 71 | [ text (service.name ++ slotLabel slot) 72 | , br [] [] 73 | , text (statusString status.state desiredState) ] 74 | , timestateInfo 75 | ]) 76 | ]) 77 | ]) 78 | 79 | 80 | 81 | serviceNode : Service -> TaskIndex -> Node -> Html msg 82 | serviceNode service taskAllocations node = 83 | let 84 | tasks = 85 | Maybe.withDefault [] (Dict.get ( node.id, service.id ) taskAllocations) 86 | forThisService (n, s) = 87 | s == service.id 88 | tasksOfThisService = List.filter forThisService (Dict.keys taskAllocations) 89 | noTaskNowhere = List.length tasksOfThisService == 0 90 | in 91 | if noTaskNowhere then 92 | td [ class "empty-service" ] [] 93 | else 94 | td [] 95 | [ ul [] (List.map (task service) tasks) ] 96 | 97 | 98 | serviceRow : List Node -> TaskIndex -> Networks.Connections -> Service -> Html msg 99 | serviceRow nodes taskAllocations networkConnections service = 100 | tr [] 101 | (th [] [ text service.name ] :: (Networks.connections service networkConnections) :: (List.map (serviceNode service taskAllocations) nodes)) 102 | 103 | 104 | node : Node -> Html msg 105 | node node = 106 | let 107 | leader = 108 | Maybe.withDefault False (Maybe.map .leader node.managerStatus) 109 | 110 | classes = 111 | [ ( "down", node.status.state == "down" ) 112 | , ( "manager", node.role == "manager" ) 113 | , ( "leader", leader ) 114 | ] 115 | 116 | nodeRole = 117 | String.join " " [ node.role, iff leader "(leader)" "" ] 118 | 119 | info = 120 | case node.info of 121 | Just s -> 122 | [ 123 | br [] [] 124 | , text (s) 125 | ] 126 | 127 | Nothing -> 128 | [] 129 | in 130 | th [ classList classes ] 131 | (List.concat [ 132 | [ 133 | strong [] [ text node.name ] 134 | , br [] [] 135 | , text nodeRole 136 | , br [] [] 137 | , text node.status.address 138 | ] 139 | , info 140 | ]) 141 | 142 | 143 | swarmHeader : List Node -> List Network -> String -> Html msg 144 | swarmHeader nodes networks refreshTime = 145 | tr [] ((th [] [ img [ src "docker_logo.svg" ] [] 146 | , div [ class "refresh-time" ] [ text refreshTime ] 147 | ] 148 | ) :: Networks.header networks :: (nodes |> List.map node)) 149 | 150 | 151 | swarmGrid : List Service -> List Node -> List Network -> TaskIndex -> String -> Html msg 152 | swarmGrid services nodes networks taskAllocations refreshTime = 153 | let 154 | networkConnections = 155 | Networks.buildConnections services networks 156 | in 157 | table [] 158 | [ thead [] [ swarmHeader nodes networks refreshTime ] 159 | , tbody [] (List.map (serviceRow nodes taskAllocations networkConnections) services) 160 | ] 161 | -------------------------------------------------------------------------------- /elm-client/Components/NetworkConnections.elm: -------------------------------------------------------------------------------- 1 | module Components.NetworkConnections exposing (Connection(..), NetworkConnections, build, serviceConnections) 2 | 3 | import Dict exposing (Dict) 4 | import Docker.Types exposing (..) 5 | import Util exposing (..) 6 | 7 | 8 | type alias Bounds = 9 | ( Int, Int ) 10 | 11 | 12 | type Connection 13 | = None 14 | | Through 15 | | Start 16 | | Middle 17 | | End 18 | | Only 19 | 20 | 21 | type alias NetworkConnections = 22 | { networks : List Network 23 | , connections : NetworkConnectionTypes 24 | } 25 | 26 | 27 | type alias NetworkConnectionTypes = 28 | Dict ( ServiceId, NetworkId ) Connection 29 | 30 | 31 | type alias NetworkAttachments = 32 | Dict ( ServiceId, NetworkId ) Bool 33 | 34 | 35 | attachments : List Service -> NetworkAttachments 36 | attachments services = 37 | let 38 | networkReducer : ServiceId -> Network -> NetworkAttachments -> NetworkAttachments 39 | networkReducer serviceId network attachments = 40 | Dict.update ( serviceId, network.id ) (always (Just True)) attachments 41 | 42 | serviceReducer : Service -> NetworkAttachments -> NetworkAttachments 43 | serviceReducer service attachments = 44 | service.networks |> List.foldl (networkReducer service.id) attachments 45 | in 46 | List.foldl serviceReducer Dict.empty services 47 | 48 | 49 | connectionType : Service -> Network -> Bool -> Int -> Bounds -> Connection 50 | connectionType service network connected idx ( first, last ) = 51 | if idx < first || idx > last then 52 | None 53 | else if idx == first && idx == last then 54 | Only 55 | else if idx == first then 56 | Start 57 | else if idx == last then 58 | End 59 | else if connected then 60 | Middle 61 | else 62 | Through 63 | 64 | 65 | empty : NetworkConnectionTypes 66 | empty = 67 | Dict.empty 68 | 69 | 70 | get : ( ServiceId, NetworkId ) -> NetworkConnectionTypes -> Connection 71 | get key connections = 72 | Maybe.withDefault None (Dict.get key connections) 73 | 74 | 75 | update : ServiceId -> NetworkId -> Connection -> NetworkConnectionTypes -> NetworkConnectionTypes 76 | update sid nid connection connections = 77 | Dict.update ( sid, nid ) (always (Just connection)) connections 78 | 79 | 80 | 81 | -- Public interface 82 | 83 | 84 | serviceConnections : Service -> NetworkConnections -> List Connection 85 | serviceConnections service { networks, connections } = 86 | List.map (\n -> get ( service.id, n.id ) connections) networks 87 | 88 | 89 | build : List Service -> List Network -> NetworkConnections 90 | build services networks = 91 | let 92 | networkAttachments = 93 | attachments services 94 | 95 | attached sid nid = 96 | Maybe.withDefault False (Dict.get ( sid, nid ) networkAttachments) 97 | 98 | updateBounds : Int -> Bool -> Bool -> Bounds -> Bounds 99 | updateBounds current connected ingress ( first, last ) = 100 | let 101 | hasLowerBound = 102 | not ingress && first < 0 103 | in 104 | ( (iff (connected && hasLowerBound) current first), (iff connected current last) ) 105 | 106 | firstAndLastConnection : Network -> Bounds 107 | firstAndLastConnection n = 108 | services 109 | |> Util.indexedFoldl 110 | (\idx s bounds -> updateBounds idx (attached s.id n.id) n.ingress bounds) 111 | ( -1, -1 ) 112 | 113 | updateConnections : Network -> NetworkConnectionTypes -> NetworkConnectionTypes 114 | updateConnections n connections = 115 | let 116 | bounds = 117 | (firstAndLastConnection n) 118 | in 119 | services 120 | |> Util.indexedFoldl 121 | (\nidx s connections -> update s.id n.id (connectionType s n (attached s.id n.id) nidx bounds) connections) 122 | connections 123 | in 124 | NetworkConnections 125 | networks 126 | (networks |> List.foldl updateConnections empty) 127 | -------------------------------------------------------------------------------- /elm-client/Components/Networks.elm: -------------------------------------------------------------------------------- 1 | module Components.Networks exposing (Connections, buildConnections, connections, header) 2 | 3 | import Array exposing (Array) 4 | import Html as H 5 | import Html.Attributes as A 6 | import Svg exposing (..) 7 | import Svg.Attributes exposing (..) 8 | import Docker.Types exposing (..) 9 | import Components.NetworkConnections as NetworkConnections exposing (..) 10 | import Util exposing (..) 11 | 12 | 13 | type alias Connections = 14 | NetworkConnections 15 | 16 | 17 | type alias Color = 18 | String 19 | 20 | 21 | networkColors : Array Color 22 | networkColors = 23 | Array.fromList 24 | [ "rgb(215, 74, 136)" 25 | , "rgb(243, 154, 155)" 26 | , "rgb(169, 65, 144)" 27 | , "rgb(249, 199, 160)" 28 | , "rgb(263, 110, 141)" 29 | ] 30 | 31 | 32 | networkColor : Int -> Color 33 | networkColor i = 34 | Maybe.withDefault "white" (Array.get (i % Array.length networkColors) networkColors) 35 | 36 | 37 | 38 | -- Geometry 39 | 40 | 41 | widthStep : Float 42 | widthStep = 43 | 16 44 | 45 | 46 | totalWidth : List a -> Float 47 | totalWidth aList = 48 | (toFloat (List.length aList)) * widthStep 49 | 50 | 51 | columnCenter : Int -> Float 52 | columnCenter i = 53 | ((toFloat i) * widthStep + widthStep / 2) 54 | 55 | 56 | columnStart : Int -> Float 57 | columnStart i = 58 | ((toFloat i) * widthStep) 59 | 60 | 61 | 62 | -- SVG shorthand 63 | 64 | 65 | svgLine : ( Float, Float ) -> ( Float, Float ) -> Float -> String -> String -> Svg msg 66 | svgLine ( ox, oy ) ( dx, dy ) width colour name = 67 | line 68 | [ x1 (toString ox) 69 | , y1 (toString oy) 70 | , x2 (toString dx) 71 | , y2 (toString dy) 72 | , strokeWidth (toString width) 73 | , stroke colour 74 | ] 75 | [ 76 | Svg.title [] [ text name ] 77 | ] 78 | 79 | 80 | svgCircle : ( Float, Float ) -> Float -> String -> String -> Svg msg 81 | svgCircle ( cenx, ceny ) rad colour name = 82 | circle 83 | [ cx (toString cenx) 84 | , cy (toString ceny) 85 | , r (toString rad) 86 | , fill colour 87 | ] 88 | [ 89 | Svg.title [] [ text name ] 90 | ] 91 | 92 | 93 | 94 | -- Symbol pieces 95 | 96 | 97 | topLine : Int -> Color -> String -> Svg msg 98 | topLine i color name = 99 | svgLine ( columnCenter i, 0 ) ( columnCenter i, 31 ) 2 color name 100 | 101 | 102 | bottomLine : Int -> Color -> String -> Svg msg 103 | bottomLine i color name = 104 | svgLine ( columnCenter i, 31 ) ( columnCenter i, 62 ) 2 color name 105 | 106 | 107 | dot : Int -> Color -> String -> Svg msg 108 | dot i color name = 109 | svgCircle ( columnCenter i, 31 ) (widthStep / 3) color name 110 | 111 | 112 | fullLine : Int -> Color -> String -> Svg msg 113 | fullLine i color name = 114 | svgLine ( columnCenter i, 0 ) ( columnCenter i, 1 ) 2 color name 115 | 116 | 117 | tcap : Int -> Color -> String -> List (Svg msg) 118 | tcap i color name = 119 | [ (svgLine ( (columnStart i) + widthStep / 6, 0 ) ( (columnStart i) + widthStep * 5 / 6, 0 ) 4 color name) 120 | , svgLine ( columnCenter i, 0 ) ( columnCenter i, widthStep ) 2 color name 121 | ] 122 | 123 | 124 | 125 | -- Components 126 | 127 | 128 | head : List Network -> Svg msg 129 | head networks = 130 | let 131 | cap i network = 132 | if network.ingress then 133 | tcap i "white" network.name 134 | else 135 | [] 136 | in 137 | svg 138 | [ width (toString (totalWidth networks)) 139 | , height (toString widthStep) 140 | , viewBox ("0 0 " ++ toString (totalWidth networks) ++ " " ++ toString widthStep) 141 | ] 142 | (networks |> List.indexedMap cap >> List.concat) 143 | 144 | 145 | attachments : List Connection -> Array Color -> Array String -> Svg msg 146 | attachments connections colors names = 147 | let 148 | symbol : Int -> Connection -> List (Svg msg) 149 | symbol i connection = 150 | let 151 | color = 152 | Maybe.withDefault "white" (Array.get i colors) 153 | 154 | name = 155 | Maybe.withDefault "" (Array.get i names) 156 | in 157 | case connection of 158 | Through -> 159 | [ topLine i color name, bottomLine i color name ] 160 | 161 | Start -> 162 | [ dot i color name, bottomLine i color name ] 163 | 164 | Middle -> 165 | [ topLine i color name, dot i color name, bottomLine i color name ] 166 | 167 | End -> 168 | [ topLine i color name, dot i color name ] 169 | 170 | Only -> 171 | [ dot i color name ] 172 | 173 | None -> 174 | [] 175 | in 176 | svg 177 | [ width (toString (totalWidth connections)), height "62", viewBox ("0 0 " ++ toString (totalWidth connections) ++ " 62") ] 178 | (connections |> List.indexedMap symbol >> List.concat) 179 | 180 | 181 | tails : List Connection -> Array Color -> Array String -> Svg msg 182 | tails connections colors names = 183 | let 184 | symbol i connection = 185 | let 186 | color = 187 | Maybe.withDefault "white" (Array.get i colors) 188 | 189 | name = 190 | Maybe.withDefault "" (Array.get i names) 191 | in 192 | if List.member connection [ Start, Middle, Through ] then 193 | [ fullLine i color name ] 194 | else 195 | [] 196 | in 197 | svg 198 | [ width (toString (totalWidth connections)) 199 | , height "100%" 200 | , viewBox ("0 0 " ++ toString (totalWidth connections) ++ " 1") 201 | , preserveAspectRatio "none" 202 | ] 203 | (connections |> List.indexedMap symbol >> List.concat) 204 | 205 | 206 | 207 | -- Exposed compopnents 208 | 209 | 210 | buildConnections : List Service -> List Network -> Connections 211 | buildConnections = 212 | NetworkConnections.build 213 | 214 | 215 | header : List Network -> H.Html msg 216 | header networks = 217 | H.th [ class "networks", A.style [ ( "width", (toString (totalWidth networks)) ++ "px" ) ] ] [ head networks ] 218 | 219 | 220 | connections : Service -> Connections -> H.Html msg 221 | connections service networkConnections = 222 | let 223 | connections = 224 | NetworkConnections.serviceConnections service networkConnections 225 | 226 | colors = 227 | networkConnections.networks |> Array.fromList << List.indexedMap (\i n -> iff n.ingress "white" (networkColor i)) 228 | 229 | names = 230 | networkConnections.networks |> Array.fromList << List.indexedMap (\i n -> n.name) 231 | in 232 | H.td [ class "networks" ] 233 | [ attachments connections colors names 234 | , H.div [] [ tails connections colors names ] 235 | ] 236 | -------------------------------------------------------------------------------- /elm-client/Docker.elm: -------------------------------------------------------------------------------- 1 | module Docker exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Date exposing (Date) 5 | import Docker.Types exposing (..) 6 | import Docker.Json exposing (parse) 7 | import Util exposing (..) 8 | 9 | 10 | isFailed : TaskStatus -> Bool 11 | isFailed { state } = 12 | (state == "failed") 13 | 14 | 15 | isCompleted : TaskStatus -> Bool 16 | isCompleted { state } = 17 | (state == "rejected") || (state == "shutdown") 18 | 19 | 20 | withoutFailedTaskHistory : List AssignedTask -> List AssignedTask 21 | withoutFailedTaskHistory = 22 | let 23 | key { serviceId, slot } = 24 | ( serviceId, (Maybe.withDefault 0 slot) ) 25 | 26 | latestRunning = 27 | List.sortBy (.status >> .timestamp >> Date.toTime) 28 | >> List.filter (\t -> t.status.state /= "failed") 29 | >> List.reverse 30 | >> List.head 31 | 32 | latest = 33 | List.sortBy (.status >> .timestamp >> Date.toTime) 34 | >> List.reverse 35 | >> (List.take 1) 36 | 37 | failedOlderThan running task = 38 | isFailed task.status && Date.toTime task.status.timestamp < Date.toTime running.status.timestamp 39 | 40 | filterPreviouslyFailed tasks = 41 | case latestRunning tasks of 42 | -- remove older failed tasks 43 | Just runningTask -> 44 | List.filter (complement (failedOlderThan runningTask)) tasks 45 | 46 | -- Keep only the latest failed task 47 | Nothing -> 48 | latest tasks 49 | in 50 | (groupBy key) >> (Dict.map (\_ -> filterPreviouslyFailed)) >> Dict.values >> List.concat 51 | 52 | 53 | process : DockerApiData -> Docker 54 | process { nodes, networks, services, tasks, refreshTime } = 55 | let 56 | emptyNetwork = 57 | { id = "", ingress = False, name = "" } 58 | 59 | networkIndex = 60 | indexBy (.id) networks 61 | 62 | resolveNetworks : List NetworkId -> List Network 63 | resolveNetworks networks = 64 | networks |> List.map (\id -> Maybe.withDefault emptyNetwork (Dict.get id networkIndex)) 65 | 66 | linkNetworks : List RawService -> List Service 67 | linkNetworks = 68 | List.map (\service -> { service | networks = resolveNetworks service.networks }) 69 | 70 | allNetworks : List RawService -> List Network 71 | allNetworks = 72 | List.concatMap .networks 73 | >> unique 74 | >> resolveNetworks 75 | >> (List.sortBy .name) 76 | >> (List.sortBy (.ingress >> \ingress -> iff ingress 0 1)) 77 | 78 | ( assignedTasks, plannedTasks ) = 79 | tasks 80 | |> (List.partition (.nodeId >> isJust)) 81 | >> (Tuple.mapFirst (List.map assignedTask)) 82 | >> (Tuple.mapSecond (List.map plannedTask)) 83 | 84 | notCompleted = 85 | List.filter (.status >> complement isCompleted) 86 | 87 | filterTasks = 88 | notCompleted >> withoutFailedTaskHistory 89 | in 90 | { nodes = (List.sortBy .name nodes) 91 | , networks = (allNetworks services) 92 | , services = (List.sortBy .name (linkNetworks services)) 93 | , plannedTasks = plannedTasks 94 | , assignedTasks = (filterTasks assignedTasks) 95 | , refreshTime = refreshTime 96 | } 97 | 98 | 99 | empty : Docker 100 | empty = 101 | Docker [] [] [] [] [] "" 102 | 103 | 104 | fromJson : String -> Result String Docker 105 | fromJson = 106 | parse >> Result.map process 107 | -------------------------------------------------------------------------------- /elm-client/Docker/Json.elm: -------------------------------------------------------------------------------- 1 | module Docker.Json exposing (parse) 2 | 3 | import Date exposing (Date) 4 | import Json.Decode as Json 5 | import Docker.Types exposing (..) 6 | 7 | 8 | containerSpec : Json.Decoder ContainerSpec 9 | containerSpec = 10 | Json.map ContainerSpec 11 | (Json.at [ "Image" ] Json.string) 12 | 13 | 14 | nodeStatus : Json.Decoder NodeStatus 15 | nodeStatus = 16 | Json.map2 NodeStatus 17 | (Json.at [ "State" ] Json.string) 18 | (Json.at [ "Addr" ] Json.string) 19 | 20 | 21 | managerStatus : Json.Decoder ManagerStatus 22 | managerStatus = 23 | Json.map2 ManagerStatus 24 | (Json.at [ "Leader" ] Json.bool) 25 | (Json.at [ "Reachability" ] Json.string) 26 | 27 | 28 | node : Json.Decoder Node 29 | node = 30 | Json.map6 Node 31 | (Json.at [ "ID" ] Json.string) 32 | (Json.at [ "Description", "Hostname" ] Json.string) 33 | (Json.at [ "Spec", "Role" ] Json.string) 34 | (Json.at [ "Status" ] nodeStatus) 35 | (Json.maybe (Json.at [ "ManagerStatus" ] managerStatus)) 36 | (Json.maybe (Json.at [ "info" ] Json.string)) 37 | 38 | 39 | network : Json.Decoder Network 40 | network = 41 | Json.map3 Network 42 | (Json.at [ "Id" ] Json.string) 43 | (Json.at [ "Name" ] Json.string) 44 | (Json.at [ "Ingress" ] Json.bool) 45 | 46 | 47 | filterEmptyNetworks : Maybe (List NetworkId) -> Json.Decoder (List NetworkId) 48 | filterEmptyNetworks networks = 49 | Json.succeed (Maybe.withDefault [] networks) 50 | 51 | 52 | service : Json.Decoder RawService 53 | service = 54 | Json.map4 RawService 55 | (Json.at [ "ID" ] Json.string) 56 | (Json.at [ "Spec", "Name" ] Json.string) 57 | (Json.at [ "Spec", "TaskTemplate", "ContainerSpec" ] containerSpec) 58 | ((Json.maybe (Json.at [ "Endpoint", "VirtualIPs" ] (Json.list (Json.at [ "NetworkID" ] Json.string)))) |> Json.andThen filterEmptyNetworks) 59 | 60 | 61 | date : Json.Decoder Date 62 | date = 63 | let 64 | safeFromString = 65 | Date.fromString >> (Result.withDefault (Date.fromTime 0.0)) 66 | in 67 | Json.string |> Json.map safeFromString 68 | 69 | 70 | taskStatus : Json.Decoder TaskStatus 71 | taskStatus = 72 | Json.map3 TaskStatus 73 | (Json.at [ "Timestamp" ] date) 74 | (Json.maybe (Json.at [ "timestateInfo" ] Json.string)) 75 | (Json.at [ "State" ] Json.string) 76 | 77 | 78 | taskInfo : Json.Decoder TaskInfo 79 | taskInfo = 80 | Json.map2 TaskInfo 81 | (Json.maybe (Json.at [ "cpu" ] Json.string)) 82 | (Json.maybe (Json.at [ "memory" ] Json.string)) 83 | 84 | 85 | task : Json.Decoder Task 86 | task = 87 | Json.map8 Task 88 | (Json.at [ "ID" ] Json.string) 89 | (Json.at [ "ServiceID" ] Json.string) 90 | (Json.maybe (Json.at [ "NodeID" ] Json.string)) 91 | (Json.maybe (Json.at [ "Slot" ] Json.int)) 92 | (Json.at [ "Status" ] taskStatus) 93 | (Json.at [ "DesiredState" ] Json.string) 94 | (Json.at [ "Spec", "ContainerSpec" ] containerSpec) 95 | (Json.at [ "info" ] taskInfo) 96 | 97 | 98 | dockerApi : Json.Decoder DockerApiData 99 | dockerApi = 100 | Json.map5 DockerApiData 101 | (Json.at [ "nodes" ] (Json.list node)) 102 | (Json.at [ "networks" ] (Json.list network)) 103 | (Json.at [ "services" ] (Json.list service)) 104 | (Json.at [ "tasks" ] (Json.list task)) 105 | (Json.at [ "refreshTime" ] Json.string) 106 | 107 | 108 | parse : String -> Result String DockerApiData 109 | parse = 110 | Json.decodeString dockerApi 111 | -------------------------------------------------------------------------------- /elm-client/Docker/Types.elm: -------------------------------------------------------------------------------- 1 | module Docker.Types exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Date exposing (Date) 5 | 6 | 7 | type alias NodeId = 8 | String 9 | 10 | 11 | type alias ServiceId = 12 | String 13 | 14 | 15 | type alias NetworkId = 16 | String 17 | 18 | 19 | type alias ContainerSpec = 20 | { image : String } 21 | 22 | 23 | type alias NodeStatus = 24 | { state : String 25 | , address : String 26 | } 27 | 28 | 29 | type alias ManagerStatus = 30 | { leader : Bool 31 | , reachability : String 32 | } 33 | 34 | 35 | type alias Node = 36 | { id : NodeId 37 | , name : String 38 | , role : String 39 | , status : NodeStatus 40 | , managerStatus : Maybe ManagerStatus 41 | , info : Maybe String 42 | } 43 | 44 | 45 | type alias Network = 46 | { id : NetworkId 47 | , name : String 48 | , ingress : Bool 49 | } 50 | 51 | 52 | type alias RawService = 53 | { id : ServiceId 54 | , name : String 55 | , containerSpec : ContainerSpec 56 | , networks : List NetworkId 57 | } 58 | 59 | 60 | type alias Service = 61 | { id : ServiceId 62 | , name : String 63 | , containerSpec : ContainerSpec 64 | , networks : List Network 65 | } 66 | 67 | 68 | type alias TaskStatus = 69 | { timestamp : Date 70 | , timestateInfo : Maybe String 71 | , state : String 72 | } 73 | 74 | 75 | type alias TaskInfo = 76 | { cpu : Maybe String 77 | , memory : Maybe String 78 | } 79 | 80 | 81 | type alias Task = 82 | { id : String 83 | , serviceId : String 84 | , nodeId : Maybe String 85 | , slot : Maybe Int 86 | , status : TaskStatus 87 | , desiredState : String 88 | , containerSpec : ContainerSpec 89 | , info : TaskInfo 90 | } 91 | 92 | 93 | type alias PlannedTask = 94 | { id : String 95 | , serviceId : String 96 | , slot : Maybe Int 97 | , status : TaskStatus 98 | , desiredState : String 99 | , containerSpec : ContainerSpec 100 | } 101 | 102 | 103 | plannedTask : Task -> PlannedTask 104 | plannedTask { id, serviceId, slot, status, desiredState, containerSpec } = 105 | PlannedTask id serviceId slot status desiredState containerSpec 106 | 107 | 108 | type alias AssignedTask = 109 | { id : String 110 | , serviceId : String 111 | , nodeId : String 112 | , slot : Maybe Int 113 | , status : TaskStatus 114 | , desiredState : String 115 | , containerSpec : ContainerSpec 116 | , info : TaskInfo 117 | } 118 | 119 | 120 | assignedTask : Task -> AssignedTask 121 | assignedTask { id, serviceId, nodeId, slot, status, desiredState, containerSpec, info } = 122 | AssignedTask id serviceId (Maybe.withDefault "" nodeId) slot status desiredState containerSpec info 123 | 124 | 125 | type alias Docker = 126 | { nodes : List Node 127 | , networks : List Network 128 | , services : List Service 129 | , plannedTasks : List PlannedTask 130 | , assignedTasks : List AssignedTask 131 | , refreshTime : String 132 | } 133 | 134 | 135 | type alias DockerApiData = 136 | { nodes : List Node 137 | , networks : List Network 138 | , services : List RawService 139 | , tasks : List Task 140 | , refreshTime : String 141 | } 142 | 143 | 144 | type alias TaskIndexKey = 145 | ( NodeId, ServiceId ) 146 | 147 | 148 | type alias TaskIndex = 149 | Dict TaskIndexKey (List AssignedTask) 150 | 151 | 152 | taskIndexKey : AssignedTask -> TaskIndexKey 153 | taskIndexKey { nodeId, serviceId } = 154 | ( nodeId, serviceId ) 155 | -------------------------------------------------------------------------------- /elm-client/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Navigation 4 | import Html exposing (..) 5 | import Dict exposing (Dict) 6 | import Util exposing (..) 7 | import WebSocket 8 | import Docker.Types exposing (..) 9 | import Docker exposing (fromJson) 10 | import Components as UI 11 | import Http 12 | 13 | localWebsocket : Navigation.Location -> String 14 | localWebsocket location = 15 | if location.protocol == "https:" then 16 | "wss://" ++ location.host ++ location.pathname ++ "stream" 17 | else 18 | "ws://" ++ location.host ++ location.pathname ++ "stream" 19 | 20 | 21 | type alias Model = 22 | { pathname : String 23 | , webSocketUrl : String 24 | , authToken : String 25 | , swarm : Docker 26 | , tasks : TaskIndex 27 | , errors : List String 28 | } 29 | 30 | 31 | type Msg 32 | = AuthTokenReceived (Result Http.Error String) 33 | | UrlChange Navigation.Location 34 | | Receive String 35 | 36 | authTokenGetter : String -> Cmd Msg 37 | authTokenGetter pathname = 38 | Http.send AuthTokenReceived ( Http.getString ( pathname ++ "auth_token" ) ) 39 | 40 | init : Navigation.Location -> ( Model, Cmd Msg ) 41 | init location = 42 | ( { pathname = location.pathname 43 | , webSocketUrl = localWebsocket location 44 | , authToken = "" 45 | , swarm = Docker.empty 46 | , tasks = Dict.empty 47 | , errors = [] 48 | } 49 | , authTokenGetter location.pathname 50 | ) 51 | 52 | 53 | update : Msg -> Model -> ( Model, Cmd Msg ) 54 | update msg model = 55 | case msg of 56 | AuthTokenReceived result -> 57 | case result of 58 | Ok authToken -> 59 | ( { model | authToken = authToken }, Cmd.none ) 60 | 61 | Err httpError -> 62 | ( { model | errors = (toString httpError) :: model.errors }, Cmd.none ) 63 | 64 | Receive serverJson -> 65 | case fromJson serverJson of 66 | Ok serverData -> 67 | ( { model | swarm = serverData, tasks = groupBy taskIndexKey serverData.assignedTasks }, Cmd.none ) 68 | 69 | Err error -> 70 | if String.contains "WrongAuthToken" error then -- caused by a reconnection 71 | ( model, ( authTokenGetter model.pathname ) ) 72 | else 73 | ( { model | errors = error :: model.errors }, Cmd.none ) 74 | 75 | UrlChange location -> 76 | ( model, Cmd.none ) 77 | 78 | 79 | subscriptions : Model -> Sub Msg 80 | subscriptions model = 81 | if String.isEmpty model.authToken then 82 | Sub.none 83 | else 84 | WebSocket.listen (model.webSocketUrl ++ "?authToken=" ++ model.authToken) Receive 85 | 86 | 87 | view : Model -> Html Msg 88 | view { swarm, tasks, errors } = 89 | let 90 | { services, nodes, networks, refreshTime } = 91 | swarm 92 | in 93 | div [] 94 | [ UI.swarmGrid services nodes networks tasks refreshTime 95 | , ul [] (List.map (\e -> li [] [ text e ]) errors) 96 | ] 97 | 98 | 99 | main : Program Never Model Msg 100 | main = 101 | Navigation.program UrlChange 102 | { init = init 103 | , update = update 104 | , subscriptions = subscriptions 105 | , view = view 106 | } 107 | -------------------------------------------------------------------------------- /elm-client/Util.elm: -------------------------------------------------------------------------------- 1 | module Util exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Set exposing (Set) 5 | 6 | 7 | complement : (a -> Bool) -> a -> Bool 8 | complement fn = 9 | \x -> not (fn x) 10 | 11 | 12 | isJust : Maybe a -> Bool 13 | isJust x = 14 | Maybe.withDefault False (Maybe.map (\x -> True) x) 15 | 16 | 17 | iff : Bool -> a -> a -> a 18 | iff condition true false = 19 | if condition then 20 | true 21 | else 22 | false 23 | 24 | 25 | groupBy : (a -> comparable) -> List a -> Dict comparable (List a) 26 | groupBy key = 27 | let 28 | cons new = 29 | Maybe.withDefault [] 30 | >> (::) new 31 | >> Just 32 | 33 | reducer item = 34 | Dict.update (key item) (cons item) 35 | in 36 | List.foldl reducer Dict.empty 37 | 38 | 39 | indexBy : (a -> comparable) -> List a -> Dict comparable a 40 | indexBy key = 41 | let 42 | cons new = 43 | Maybe.withDefault new 44 | >> Just 45 | 46 | reducer item = 47 | Dict.update (key item) (cons item) 48 | in 49 | List.foldl reducer Dict.empty 50 | 51 | 52 | unique : List comparable -> List comparable 53 | unique = 54 | Set.fromList >> Set.toList 55 | 56 | 57 | indexedFoldl : (Int -> a -> b -> b) -> b -> List a -> b 58 | indexedFoldl indexedReducer init list = 59 | let 60 | reducer item ( idx, accumulator ) = 61 | ( idx + 1, indexedReducer idx item accumulator ) 62 | in 63 | list |> (List.foldl reducer ( 0, init ) >> Tuple.second) 64 | -------------------------------------------------------------------------------- /elm-client/client/docker_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | Group 6 | Created with Sketch. 7 | 8 | 9 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /elm-client/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Docker Swarm 7 | 8 | 193 | 194 | 195 | 196 | 197 | 200 | 201 | -------------------------------------------------------------------------------- /elm-client/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 12 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 13 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 14 | "elm-lang/navigation": "2.1.0 <= v < 3.0.0", 15 | "elm-lang/svg": "2.0.0 <= v < 3.0.0", 16 | "elm-lang/websocket": "1.0.2 <= v < 2.0.0" 17 | }, 18 | "elm-version": "0.18.0 <= v < 0.19.0" 19 | } -------------------------------------------------------------------------------- /healthcheck.sh: -------------------------------------------------------------------------------- 1 | if [ "$ENABLE_HTTPS" == "true" ]; then 2 | curl --insecure --fail https://localhost:$PORT/_health || exit 1 3 | else 4 | curl --fail http://localhost:$PORT/_health || exit 1 5 | fi -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swarm-dashboard", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "express": "5.0.1", 9 | "express-basic-auth": "^1.2.1", 10 | "moment": "^2.29.4", 11 | "parse-prometheus-text-format": "^1.1.1", 12 | "ramda": "0.30.1", 13 | "uuid": "11.0.3", 14 | "ws": "^8.17.1" 15 | }, 16 | "devDependencies": { 17 | "babel-eslint-parser": "7.13.10", 18 | "eslint": "9.17.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | if [ "$ENABLE_HTTPS" == "true" ]; then 2 | if lego --path $LEGO_PATH list | grep -q 'No certificates found.'; then 3 | echo "running lego new command" 4 | lego --path $LEGO_PATH $LEGO_NEW_COMMAND_ARGS 5 | else 6 | echo "running lego renew command" 7 | no_random_sleep_option="--no-random-sleep" 8 | if [ "$USE_RENEW_DELAY_ON_START" == "true" ]; then 9 | no_random_sleep_option="" 10 | fi 11 | lego --path $LEGO_PATH $LEGO_RENEW_COMMAND_ARGS $no_random_sleep_option 12 | fi 13 | fi 14 | 15 | { node server/index.js; } & 16 | { crond -f -d 8; } & 17 | wait -n 18 | echo kill all 19 | pkill -P $$ 20 | echo exit parent -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, watchFile } from 'node:fs'; 2 | import { request, createServer as httpCreateServer } from 'http'; 3 | import { createServer as httpsCreateServer } from 'https'; 4 | import { createHash } from 'crypto'; 5 | import parsePrometheusTextFormat from 'parse-prometheus-text-format'; 6 | 7 | import WebSocket, { WebSocketServer } from 'ws'; 8 | import express, { Router } from 'express'; 9 | import basicAuth from 'express-basic-auth'; 10 | import { v4 as uuidv4 } from 'uuid'; 11 | import { parse } from 'url'; 12 | import { sortBy, prop } from 'ramda'; 13 | import moment from 'moment'; 14 | 15 | const port = process.env.PORT || 8080; 16 | const realm = process.env.AUTHENTICATION_REALM || "KuW2i9GdLIkql"; 17 | const enableAuthentication = process.env.ENABLE_AUTHENTICATION === "true" 18 | const username = process.env.USERNAME_FILE 19 | ? readFileSync(process.env.USERNAME_FILE, 'utf-8') 20 | : process.env.USERNAME || "admin"; 21 | const password = process.env.PASSWORD_FILE 22 | ? readFileSync(process.env.PASSWORD_FILE, 'utf-8') 23 | : process.env.PASSWORD || "supersecret"; 24 | const enableHTTPS = process.env.ENABLE_HTTPS === "true"; 25 | const legoPath = process.env.LEGO_PATH || "/lego-files"; 26 | const httpsHostname = process.env.HTTPS_HOSTNAME; 27 | const dockerUpdateInterval = parseInt(process.env.DOCKER_UPDATE_INTERVAL || "5000"); 28 | const metricsUpdateInterval = parseInt(process.env.METRICS_UPDATE_INTERVAL || "30000"); 29 | const showTaskTimestamp = !(process.env.SHOW_TASK_TIMESTAMP === "false"); 30 | const enableNetworks = !(process.env.ENABLE_NETWORKS === "false"); 31 | const debugMode = process.env.DEBUG_MODE === "true"; 32 | const enableDataAPI = process.env.ENABLE_DATA_API === "true"; 33 | 34 | const _nodeExporterServiceNameRegex = process.env.NODE_EXPORTER_SERVICE_NAME_REGEX || ""; 35 | const useNodeExporter = _nodeExporterServiceNameRegex !== ""; 36 | const nodeExporterServiceNameRegex = new RegExp(_nodeExporterServiceNameRegex); 37 | const nodeExporterInterestedMountPoint = process.env.NODE_EXPORTER_INTERESTED_MOUNT_POINT || "/"; 38 | const nodeExporterPort = process.env.NODE_EXPORTER_PORT || "9100"; 39 | 40 | const _cadvisorServiceNameRegex = process.env.CADVISOR_SERVICE_NAME_REGEX || ""; 41 | const useCadvisor = _cadvisorServiceNameRegex !== ""; 42 | const cadvisorServiceNameRegex = new RegExp(_cadvisorServiceNameRegex); 43 | const cadvisorPort = process.env.CADVISOR_PORT || "8080"; 44 | 45 | let pathPrefix = process.env.PATH_PREFIX || "/"; 46 | if (pathPrefix.endsWith("/")) { 47 | pathPrefix = pathPrefix.slice(0, -1); 48 | } 49 | 50 | 51 | const sha1OfData = data => 52 | createHash('sha1').update(JSON.stringify(data)).digest('hex'); 53 | 54 | const sum = (arr) => { 55 | var res = undefined; for (let i = 0; i < arr.length; i++) { if (res === undefined) res = 0; res += arr[i]; } return res; 56 | } 57 | 58 | function formatBytes(bytes, decimals = 0) { 59 | if (!+bytes) return '0 Bytes' 60 | const k = 1000 61 | const dm = decimals < 0 ? 0 : decimals 62 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 63 | const i = Math.floor(Math.log(bytes) / Math.log(k)) 64 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}` 65 | } 66 | 67 | // Docker API integration 68 | 69 | const dockerRequestBaseOptions = { 70 | method: 'GET', 71 | socketPath: '/var/run/docker.sock', 72 | }; 73 | 74 | const dockerAPIRequest = path => { 75 | return new Promise((res, rej) => { 76 | let buffer = ''; 77 | 78 | const r = request({ ...dockerRequestBaseOptions, path }, response => { 79 | response.on('data', chunk => (buffer = buffer + chunk)); 80 | response.on('end', () => res(buffer)); 81 | }); 82 | 83 | r.on('error', rej); 84 | 85 | r.end(); 86 | }); 87 | }; 88 | 89 | const fetchDockerData = () => 90 | Promise.all([ 91 | dockerAPIRequest('/nodes').then(JSON.parse), 92 | dockerAPIRequest('/services').then(JSON.parse), 93 | dockerAPIRequest('/networks').then(JSON.parse), 94 | dockerAPIRequest('/tasks').then(JSON.parse), 95 | ]).then(([nodes, services, networks, tasks]) => ({ 96 | nodes, 97 | services, 98 | networks, 99 | tasks, 100 | })); 101 | 102 | // Fetch metrics 103 | 104 | const metricRequest = (url) => { 105 | return new Promise((res, rej) => { 106 | let buffer = ''; 107 | 108 | const r = request(url, response => { 109 | response.on('data', chunk => (buffer = buffer + chunk)); 110 | response.on('end', () => res(buffer)); 111 | }); 112 | 113 | r.on('error', rej); 114 | 115 | r.end(); 116 | }); 117 | }; 118 | 119 | const fetchMetrics = (addresses) => { 120 | let promises = []; 121 | for (let i = 0; i < addresses.length; i++) { 122 | promises.push(metricRequest(addresses[i]).then(parsePrometheusTextFormat)); 123 | } 124 | return Promise.all(promises); 125 | } 126 | 127 | // Docker API returns networks in an undefined order, this 128 | // stabilizes the order for effective caching 129 | const stabilize = data => { 130 | return { ...data, networks: sortBy(prop('Id'), data.networks) }; 131 | }; 132 | 133 | const parseAndRedactDockerData = data => { 134 | const now = moment(); 135 | const refreshTime = now.format('YYYY-MM-DD HH:mm:ss'); 136 | 137 | let nodes = []; 138 | let networks = []; 139 | let services = []; 140 | let tasks = []; 141 | 142 | let nodeExporterServiceIDs = []; 143 | let runningNodeExportes = []; 144 | let cadvisorServiceIDs = []; 145 | let runningCadvisors = []; 146 | let runningTasksID = []; 147 | 148 | for (let i = 0; i < data.nodes.length; i++) { 149 | const baseNode = data.nodes[i]; 150 | let node = { 151 | "ID": baseNode["ID"], 152 | "Description": { 153 | "Hostname": baseNode["Description"]["Hostname"], 154 | }, 155 | "Spec": { 156 | "Role": baseNode["Spec"]["Role"], 157 | }, 158 | "Status": { 159 | "State": baseNode["Status"]["State"], 160 | "Addr": baseNode["Status"]["Addr"], 161 | }, 162 | }; 163 | if (baseNode["ManagerStatus"] !== undefined) { 164 | node["ManagerStatus"] = { 165 | "Leader": baseNode["ManagerStatus"]["Leader"], 166 | "Reachability": baseNode["ManagerStatus"]["Reachability"], 167 | } 168 | } 169 | nodes.push(node); 170 | } 171 | 172 | if (enableNetworks) { 173 | for (let i = 0; i < data.networks.length; i++) { 174 | const baseNetwork = data.networks[i]; 175 | let network = { 176 | "Id": baseNetwork["Id"], 177 | "Name": baseNetwork["Name"], 178 | "Ingress": baseNetwork["Ingress"], 179 | }; 180 | networks.push(network); 181 | } 182 | } 183 | 184 | for (let i = 0; i < data.services.length; i++) { 185 | const baseService = data.services[i]; 186 | let service = { 187 | "ID": baseService["ID"], 188 | "Spec": { 189 | "Name": baseService["Spec"]["Name"], 190 | "TaskTemplate": { 191 | "ContainerSpec": { 192 | "Image": baseService["Spec"]["TaskTemplate"]["ContainerSpec"]["Image"] 193 | }, 194 | }, 195 | }, 196 | }; 197 | if (enableNetworks && (baseService["Endpoint"] !== undefined)) { 198 | const _baseVIPs = baseService["Endpoint"]["VirtualIPs"]; 199 | if (_baseVIPs !== undefined && Array.isArray(_baseVIPs) && _baseVIPs.length > 0) { 200 | let vips = [] 201 | for (let j = 0; j < _baseVIPs.length; j++) { 202 | const _baseVIP = _baseVIPs[j]; 203 | vips.push({ 204 | "NetworkID": _baseVIP["NetworkID"], 205 | }) 206 | } 207 | service["Endpoint"] = { 208 | "VirtualIPs": vips 209 | } 210 | } 211 | } 212 | services.push(service); 213 | 214 | if (useNodeExporter) { 215 | if (nodeExporterServiceNameRegex.test(baseService["Spec"]["Name"])) { 216 | nodeExporterServiceIDs.push(baseService["ID"]); 217 | } 218 | } 219 | if (useCadvisor) { 220 | if (cadvisorServiceNameRegex.test(baseService["Spec"]["Name"])) { 221 | cadvisorServiceIDs.push(baseService["ID"]); 222 | } 223 | } 224 | } 225 | 226 | for (let i = 0; i < data.tasks.length; i++) { 227 | const baseTask = data.tasks[i]; 228 | const lastTimestamp = moment(baseTask["Status"]["Timestamp"]); 229 | let timestateInfo = undefined; 230 | if (showTaskTimestamp) { 231 | timestateInfo = moment.duration(lastTimestamp - now).humanize(true); 232 | } 233 | let task = { 234 | "ID": baseTask["ID"], 235 | "ServiceID": baseTask["ServiceID"], 236 | "Status": { 237 | "Timestamp": baseTask["Status"]["Timestamp"], 238 | "State": baseTask["Status"]["State"], 239 | "timestateInfo": timestateInfo, 240 | }, 241 | "DesiredState": baseTask["DesiredState"], 242 | "Spec": { 243 | "ContainerSpec": { 244 | "Image": baseTask["Spec"]["ContainerSpec"]["Image"] 245 | } 246 | }, 247 | "info": {} // for cpu and memory 248 | }; 249 | if (baseTask["NodeID"] !== undefined) 250 | task["NodeID"] = baseTask["NodeID"] 251 | if (baseTask["Slot"] !== undefined) 252 | task["Slot"] = baseTask["Slot"] 253 | tasks.push(task); 254 | 255 | // get addresses for metrics 256 | if (nodeExporterServiceIDs.length > 0) { 257 | if ((nodeExporterServiceIDs.includes(baseTask["ServiceID"])) && 258 | (baseTask["Status"]["State"] === "running") && 259 | (baseTask["NodeID"] !== undefined) && 260 | (baseTask["NetworksAttachments"] !== undefined)) { 261 | let ipList = []; 262 | // TODO: we use ip of the accessible network instead of ipList[0] 263 | for (let j = 0; j < baseTask["NetworksAttachments"].length; j++) { 264 | for (let k = 0; k < baseTask["NetworksAttachments"][j]["Addresses"].length; k++) { 265 | let ip = baseTask["NetworksAttachments"][j]["Addresses"][k]; 266 | ipList.push(ip.split("/")[0]); 267 | } 268 | } 269 | runningNodeExportes.push({ nodeID: baseTask["NodeID"], address: ipList[0] }); 270 | } 271 | } 272 | if (cadvisorServiceIDs.length > 0) { 273 | if ((cadvisorServiceIDs.includes(baseTask["ServiceID"])) && 274 | (baseTask["Status"]["State"] === "running") && 275 | (baseTask["NetworksAttachments"] !== undefined)) { 276 | let ipList = []; 277 | // TODO: we use ip of the accessible network instead of ipList[0] 278 | for (let j = 0; j < baseTask["NetworksAttachments"].length; j++) { 279 | for (let k = 0; k < baseTask["NetworksAttachments"][j]["Addresses"].length; k++) { 280 | let ip = baseTask["NetworksAttachments"][j]["Addresses"][k]; 281 | ipList.push(ip.split("/")[0]); 282 | } 283 | } 284 | runningCadvisors.push({ address: ipList[0] }); 285 | } 286 | } 287 | if (baseTask["Status"]["State"] === "running") { 288 | runningTasksID.push(baseTask["ID"]); 289 | } 290 | } 291 | 292 | return { 293 | data: { nodes, networks, services, tasks, refreshTime }, 294 | runningNodeExportes, runningCadvisors, runningTasksID 295 | }; 296 | }; 297 | 298 | const findMetricValue = (metrics, name, searchLabels) => { 299 | let values = findAllMetricValue(metrics, name, searchLabels); 300 | if (values.length > 0) { 301 | return values[0] 302 | } 303 | return undefined; 304 | } 305 | 306 | 307 | const findAllMetricValue = (metrics, name, searchLabels) => { 308 | let results = []; 309 | for (let i = 0; i < metrics.length; i++) { 310 | const metricsParent = metrics[i]; 311 | if (metricsParent.name === name) { 312 | for (let j = 0; j < metricsParent.metrics.length; j++) { 313 | const metric = metricsParent.metrics[j]; 314 | let allLabelsExists = true; 315 | if (metric.labels !== undefined) { 316 | for (let k = 0; k < searchLabels.length; k++) { 317 | const label = searchLabels[k]; 318 | if (label.value !== undefined) { 319 | if (metric.labels[label.name] !== label.value) { 320 | allLabelsExists = false; 321 | } 322 | } else if (label.notValue !== undefined) { 323 | if (metric.labels[label.name] === label.notValue) { 324 | allLabelsExists = false; 325 | } 326 | } 327 | } 328 | } 329 | if (allLabelsExists) { 330 | results.push(parseFloat(metric.value)); 331 | } 332 | } 333 | } 334 | } 335 | return results; 336 | } 337 | 338 | 339 | const currentTime = () => Math.floor(Date.now() / 1000); 340 | 341 | const fetchNodeMetrics = ({ lastData, lastRunningNodeExportes, lastNodeMetrics }, callback) => { 342 | let nodeMetrics = []; 343 | if (lastRunningNodeExportes.length > 0) { // should fetch metrics 344 | fetchMetrics(lastRunningNodeExportes.map(({ address }) => `http://${address}:${nodeExporterPort}/metrics`)) 345 | .then(metricsList => { 346 | for (let i = 0; i < lastData.nodes.length; i++) { 347 | let node = lastData.nodes[i]; 348 | for (let j = 0; j < lastRunningNodeExportes.length; j++) { 349 | const nodeExporterTask = lastRunningNodeExportes[j]; 350 | if (node["ID"] === nodeExporterTask.nodeID) { 351 | const metricsOfThisNode = metricsList[j]; 352 | const metricToSave = { nodeID: node["ID"], fetchTime: currentTime() }; 353 | 354 | // last metrics 355 | let lastMetricsOfThisNode = {}; 356 | let timeDiffFromLastMetrics = 0; 357 | for (let k = 0; k < lastNodeMetrics.length; k++) { 358 | if (lastNodeMetrics[k].nodeID === node["ID"]) { 359 | lastMetricsOfThisNode = lastNodeMetrics[k]; 360 | timeDiffFromLastMetrics = metricToSave.fetchTime - lastMetricsOfThisNode.fetchTime 361 | break; 362 | } 363 | } 364 | 365 | // disk 366 | let freeDisk = findMetricValue(metricsOfThisNode, "node_filesystem_avail_bytes", [{ name: "mountpoint", value: nodeExporterInterestedMountPoint }]); 367 | let totalDisk = findMetricValue(metricsOfThisNode, "node_filesystem_size_bytes", [{ name: "mountpoint", value: nodeExporterInterestedMountPoint }]); 368 | if ((freeDisk !== undefined) && (totalDisk !== undefined)) { 369 | metricToSave.diskFullness = Math.round((totalDisk - freeDisk) * 100 / totalDisk); 370 | } 371 | 372 | // cpu 373 | metricToSave.cpuSecondsTotal = sum(findAllMetricValue(metricsOfThisNode, "node_cpu_seconds_total", [{ name: "mode", notValue: "idle" }])); 374 | if ( 375 | (metricToSave.cpuSecondsTotal !== undefined) && 376 | (lastMetricsOfThisNode.cpuSecondsTotal !== undefined) && 377 | (timeDiffFromLastMetrics > 0) 378 | ) { 379 | metricToSave.cpuPercent = Math.round((metricToSave.cpuSecondsTotal - lastMetricsOfThisNode.cpuSecondsTotal) * 100 / timeDiffFromLastMetrics); 380 | } 381 | 382 | // memory 383 | let node_memory_MemFree_bytes = findMetricValue(metricsOfThisNode, "node_memory_MemFree_bytes", []); 384 | let node_memory_Cached_bytes = findMetricValue(metricsOfThisNode, "node_memory_Cached_bytes", []); 385 | let node_memory_Buffers_bytes = findMetricValue(metricsOfThisNode, "node_memory_Buffers_bytes", []); 386 | let node_memory_MemTotal_bytes = findMetricValue(metricsOfThisNode, "node_memory_MemTotal_bytes", []); 387 | if ( 388 | (node_memory_MemFree_bytes !== undefined) && 389 | (node_memory_Cached_bytes !== undefined) && 390 | (node_memory_Buffers_bytes !== undefined) && 391 | (node_memory_MemTotal_bytes !== undefined) && 392 | (node_memory_MemTotal_bytes > 0) 393 | ) { 394 | metricToSave.memoryPercent = Math.round( 395 | 100 * (1 - ((node_memory_MemFree_bytes + node_memory_Cached_bytes + node_memory_Buffers_bytes) / node_memory_MemTotal_bytes)) 396 | ); 397 | } 398 | 399 | nodeMetrics.push(metricToSave); 400 | } 401 | } 402 | } 403 | callback(nodeMetrics); 404 | }) 405 | .catch(e => { 406 | console.error('Could not fetch node metrics', e) 407 | callback(nodeMetrics); 408 | }); 409 | } else { 410 | callback(nodeMetrics); 411 | } 412 | } 413 | 414 | const fetchTasksMetrics = ({ lastRunningCadvisors, lastRunningTasksMetrics, lastRunningTasksID }, callback) => { 415 | let runningTasksMetrics = []; 416 | if (lastRunningCadvisors.length > 0) { // should fetch metrics 417 | fetchMetrics(lastRunningCadvisors.map(({ address }) => `http://${address}:${cadvisorPort}/metrics`)) 418 | .then(metricsList => { 419 | let allMetrics = []; 420 | for (let i = 0; i < metricsList.length; i++) { 421 | allMetrics = allMetrics.concat(metricsList[i]); 422 | } 423 | for (let i = 0; i < lastRunningTasksID.length; i++) { 424 | let taskID = lastRunningTasksID[i]; 425 | const metricToSave = { taskID, fetchTime: currentTime() }; 426 | 427 | // last metrics 428 | let lastMetricsOfThisTask = {}; 429 | let timeDiffFromLastMetrics = 0; 430 | for (let k = 0; k < lastRunningTasksMetrics.length; k++) { 431 | if (lastRunningTasksMetrics[k].taskID === taskID) { 432 | lastMetricsOfThisTask = lastRunningTasksMetrics[k]; 433 | timeDiffFromLastMetrics = metricToSave.fetchTime - lastMetricsOfThisTask.fetchTime 434 | break; 435 | } 436 | } 437 | 438 | // cpu 439 | metricToSave.cpuSecondsTotal = sum(findAllMetricValue(allMetrics, "container_cpu_usage_seconds_total", [{ name: "container_label_com_docker_swarm_task_id", value: taskID }])); 440 | if ( 441 | (lastMetricsOfThisTask.cpuSecondsTotal !== undefined) && 442 | (timeDiffFromLastMetrics > 0) 443 | ) { 444 | metricToSave.cpuPercent = Math.round((metricToSave.cpuSecondsTotal - lastMetricsOfThisTask.cpuSecondsTotal) * 100 / timeDiffFromLastMetrics); 445 | } 446 | 447 | // memory 448 | metricToSave.memoryBytes = findMetricValue(allMetrics, "container_memory_rss", [{ name: "container_label_com_docker_swarm_task_id", value: taskID }]); 449 | // let memoryUsage = findMetricValue(allMetrics, "container_memory_usage_bytes", [{ name: "container_label_com_docker_swarm_task_id", value: taskID }]); 450 | // let memoryCache = findMetricValue(allMetrics, "container_memory_cache", [{ name: "container_label_com_docker_swarm_task_id", value: taskID }]); 451 | // console.log(memoryUsage, memoryCache); 452 | // if ( 453 | // (memoryUsage !== undefined) && 454 | // (memoryCache !== undefined) 455 | // ) { 456 | // metricToSave.memoryBytes = memoryUsage - memoryCache 457 | // } 458 | 459 | runningTasksMetrics.push(metricToSave); 460 | } 461 | callback(runningTasksMetrics); 462 | }) 463 | .catch(e => { 464 | console.error('Could not fetch tasks metrics', e) 465 | callback(runningTasksMetrics); 466 | }); 467 | } else { 468 | callback(runningTasksMetrics); 469 | } 470 | } 471 | 472 | const addNodeMetricsToData = (data, lastNodeMetrics) => { 473 | for (let i = 0; i < data.nodes.length; i++) { 474 | const node = data.nodes[i]; 475 | for (let j = 0; j < lastNodeMetrics.length; j++) { 476 | const nodeMetric = lastNodeMetrics[j]; 477 | if (nodeMetric.nodeID === node["ID"]) { 478 | let info = ""; 479 | if (nodeMetric.diskFullness !== undefined) { 480 | info += `disk: ${nodeMetric.diskFullness}%`; 481 | } 482 | if (nodeMetric.cpuPercent !== undefined) { 483 | if (info) 484 | info += " | " 485 | info += `cpu: ${nodeMetric.cpuPercent}%`; 486 | } 487 | if (nodeMetric.memoryPercent !== undefined) { 488 | if (info) 489 | info += " | " 490 | info += `mem: ${nodeMetric.memoryPercent}%`; 491 | } 492 | if (info) { 493 | node.info = info; 494 | } 495 | } 496 | } 497 | } 498 | } 499 | const addTaskMetricsToData = (data, lastRunningTasksMetrics) => { 500 | for (let i = 0; i < data.tasks.length; i++) { 501 | const task = data.tasks[i]; 502 | for (let j = 0; j < lastRunningTasksMetrics.length; j++) { 503 | const taskMetric = lastRunningTasksMetrics[j]; 504 | if (taskMetric.taskID === task["ID"]) { 505 | if (taskMetric.cpuPercent !== undefined) { 506 | task.info.cpu = `cpu: ${taskMetric.cpuPercent}%`; 507 | } 508 | if (taskMetric.memoryBytes !== undefined) { 509 | task.info.memory = `mem: ${formatBytes(taskMetric.memoryBytes)}`; 510 | } 511 | } 512 | } 513 | } 514 | } 515 | 516 | // WebSocket pub-sub 517 | 518 | const publish = (listeners, data) => { 519 | listeners.forEach(listener => { 520 | if (listener.readyState !== WebSocket.OPEN) return; 521 | 522 | listener.send(JSON.stringify(data, null, 2)); 523 | }); 524 | }; 525 | 526 | const subscribe = (listeners, newListener) => { 527 | return listeners.concat([newListener]); 528 | }; 529 | 530 | const unsubscribe = (listeners, listener) => { 531 | const id = listeners.indexOf(listener); 532 | if (id < 0) return listeners; 533 | 534 | return [].concat(listeners).splice(id, 1); 535 | }; 536 | 537 | const dropClosed = listeners => { 538 | return listeners.filter(ws => ws.readyState === ws.OPEN); 539 | }; 540 | 541 | // set up the application 542 | 543 | let lastRunningNodeExportes = []; 544 | let lastNodeMetrics = []; 545 | let lastRunningCadvisors = []; 546 | let lastRunningTasksID = []; 547 | let lastRunningTasksMetrics = []; 548 | 549 | let listeners = []; 550 | let lastData = {}; 551 | let lastSha = ''; 552 | 553 | let users = {}; 554 | users[username] = password; 555 | const basicAuthConfig = () => basicAuth({ 556 | users: users, 557 | challenge: true, 558 | realm: realm, 559 | }) 560 | const tokenStore = new Set(); 561 | 562 | const app = express(); 563 | const router = Router(); 564 | router.use(express.static('client')); 565 | router.get('/_health', (req, res) => res.end()); 566 | if (enableAuthentication) { 567 | router.get('/auth_token', basicAuthConfig(), (req, res) => { 568 | const token = uuidv4(); 569 | tokenStore.add(token); 570 | res.send(token); 571 | }); 572 | if (enableDataAPI) { 573 | router.get('/data', basicAuthConfig(), (req, res) => { 574 | res.send(lastData); 575 | }); 576 | } 577 | } else { 578 | router.get('/auth_token', (req, res) => { 579 | res.send("no-auth-token-needed"); 580 | }); 581 | if (enableDataAPI) { 582 | router.get('/data', (req, res) => { 583 | res.send(lastData); 584 | }); 585 | } 586 | } 587 | if (debugMode) { 588 | console.log("debug mode is active"); 589 | router.get('/debug-log', (req, res) => { 590 | console.log("lastRunningNodeExportes", lastRunningNodeExportes); 591 | console.log("lastNodeMetrics", lastNodeMetrics); 592 | console.log("lastRunningCadvisors", lastRunningCadvisors); 593 | console.log("lastRunningTasksID", lastRunningTasksID); 594 | console.log("lastRunningTasksMetrics", lastRunningTasksMetrics); 595 | console.log("---------------"); 596 | res.send("logged.") 597 | }); 598 | } 599 | 600 | app.use(pathPrefix + "/", router); 601 | 602 | // start the polling 603 | setInterval(() => { // update docker data 604 | fetchDockerData() 605 | .then(it => { 606 | let { data, runningNodeExportes, runningCadvisors, runningTasksID } = parseAndRedactDockerData(it); 607 | addNodeMetricsToData(data, lastNodeMetrics); // it makes fetching of main data and node metrics independent. 608 | addTaskMetricsToData(data, lastRunningTasksMetrics); // it makes fetching of main data and node metrics independent. 609 | 610 | data = stabilize(data); 611 | const sha = sha1OfData(data); 612 | 613 | if (sha == lastSha) return; 614 | 615 | lastSha = sha; 616 | lastData = data; 617 | lastRunningNodeExportes = runningNodeExportes; 618 | lastRunningCadvisors = runningCadvisors; 619 | lastRunningTasksID = runningTasksID; 620 | 621 | listeners = dropClosed(listeners); 622 | publish(listeners, data); 623 | }) 624 | .catch(e => console.error('Could not publish', e)); // eslint-disable-line no-console 625 | }, dockerUpdateInterval); // refreshs each 1s 626 | 627 | setInterval(() => { // update node data 628 | fetchNodeMetrics({ lastData, lastRunningNodeExportes, lastNodeMetrics }, (nodeMetrics) => { 629 | lastNodeMetrics = nodeMetrics; 630 | }) 631 | }, metricsUpdateInterval); // refreshs each 5s 632 | 633 | setInterval(() => { // update node data 634 | fetchTasksMetrics({ lastRunningCadvisors, lastRunningTasksMetrics, lastRunningTasksID }, (runningTasksMetrics) => { 635 | lastRunningTasksMetrics = runningTasksMetrics; 636 | }) 637 | }, metricsUpdateInterval); // refreshs each 5s 638 | 639 | function onWSConnection(ws, req) { 640 | let params = undefined; 641 | let authToken = undefined; 642 | if (req) 643 | params = parse(req.url, true).query; // { authToken: 'ajsdhakjsdhak' } for 'ws://localhost:1234/?authToken=ajsdhakjsdhak' 644 | if (params) 645 | authToken = params.authToken; 646 | 647 | if (!enableAuthentication || tokenStore.has(authToken)) { 648 | if (enableAuthentication) { 649 | tokenStore.delete(authToken); 650 | } 651 | 652 | listeners = subscribe(listeners, ws) || []; 653 | publish([ws], lastData); // immediately send latest to the new listener 654 | ws.on('close', () => { 655 | listeners = unsubscribe(listeners, ws) || []; 656 | }); 657 | } else { 658 | ws.send("WrongAuthToken"); 659 | setTimeout(() => { 660 | ws.close(); // terminate this connection 661 | }, 10000); 662 | } 663 | } 664 | 665 | 666 | // set up the server 667 | 668 | if (enableHTTPS) { 669 | const privateKeyPath = legoPath + '/certificates/' + httpsHostname + '.key'; 670 | const certificatePath = legoPath + '/certificates/' + httpsHostname + '.crt'; 671 | const privateKey = readFileSync(privateKeyPath, 'utf8'); 672 | const certificate = readFileSync(certificatePath, 'utf8'); 673 | const credentials = { key: privateKey, cert: certificate } 674 | const httpsServer = httpsCreateServer(credentials); 675 | httpsServer.on('request', app); 676 | const wsServer = new WebSocketServer({ 677 | path: pathPrefix + '/stream', 678 | server: httpsServer, 679 | }); 680 | wsServer.on('connection', onWSConnection); 681 | httpsServer.listen(port, () => { 682 | console.log(`HTTPS server listening on ${port}`); // eslint-disable-line no-console 683 | }); 684 | watchFile(certificatePath, { interval: 1000 }, () => { 685 | try { 686 | console.log('Reloading TLS certificate'); 687 | const privateKey = readFileSync(privateKeyPath, 'utf8'); 688 | const certificate = readFileSync(certificatePath, 'utf8'); 689 | const credentials = { key: privateKey, cert: certificate } 690 | httpsServer.setSecureContext(credentials); 691 | } catch (e) { 692 | console.log(e) 693 | } 694 | }); 695 | } else { 696 | const httpServer = httpCreateServer(); 697 | httpServer.on('request', app); 698 | const wsServer = new WebSocketServer({ 699 | path: pathPrefix + '/stream', 700 | server: httpServer, 701 | }); 702 | wsServer.on('connection', onWSConnection); 703 | httpServer.listen(port, () => { 704 | console.log(`HTTP server listening on ${port}`); // eslint-disable-line no-console 705 | }); 706 | } 707 | -------------------------------------------------------------------------------- /swarm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenasm/swarm-dashboard/65f52ca03f9c3d23b5e7cf1e7a864dcb55293346/swarm.gif -------------------------------------------------------------------------------- /test-cluster/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | .vagrant -------------------------------------------------------------------------------- /test-cluster/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.define "manager1" do |node| 6 | node.vm.box = "gusztavvargadr/docker-community-ubuntu-server" 7 | node.vm.network :private_network, ip: "10.0.0.10" 8 | node.vm.hostname = "manager1" 9 | 10 | node.vm.network "forwarded_port", guest: 8080, host: 8080 11 | 12 | node.vm.synced_folder "..", "/vagrant_parent" 13 | node.vm.synced_folder "./data", "/vagrant_data" 14 | node.vm.provision "shell", path: "wait-for-docker.sh" 15 | node.vm.provision "shell", inline: <<-SHELL 16 | docker swarm init --advertise-addr 10.0.0.10 17 | docker swarm join-token manager -q > /vagrant_data/swarm-manager-token 18 | docker swarm join-token worker -q > /vagrant_data/swarm-worker-token 19 | SHELL 20 | end 21 | 22 | config.vm.define "manager2" do |node| 23 | node.vm.box = "gusztavvargadr/docker-community-ubuntu-server" 24 | node.vm.network :private_network, ip: "10.0.0.11" 25 | node.vm.hostname = "manager2" 26 | 27 | node.vm.synced_folder "./data", "/vagrant_data" 28 | node.vm.provision "shell", path: "wait-for-docker.sh" 29 | node.vm.provision "shell", inline: <<-SHELL 30 | docker swarm join --token $(cat /vagrant_data/swarm-manager-token) 10.0.0.10:2377 31 | SHELL 32 | end 33 | 34 | config.vm.define "worker1" do |node| 35 | node.vm.box = "gusztavvargadr/docker-community-ubuntu-server" 36 | node.vm.network :private_network, ip: "10.0.0.21" 37 | node.vm.hostname = "worker1" 38 | 39 | node.vm.synced_folder "./data", "/vagrant_data" 40 | node.vm.provision "shell", path: "wait-for-docker.sh" 41 | node.vm.provision "shell", inline: <<-SHELL 42 | docker swarm join --token $(cat /vagrant_data/swarm-worker-token) 10.0.0.10:2377 43 | SHELL 44 | end 45 | 46 | config.vm.define "worker2" do |node| 47 | node.vm.box = "gusztavvargadr/docker-community-ubuntu-server" 48 | node.vm.network :private_network, ip: "10.0.0.22" 49 | node.vm.hostname = "worker2" 50 | 51 | node.vm.synced_folder "./data", "/vagrant_data" 52 | node.vm.provision "shell", path: "wait-for-docker.sh" 53 | node.vm.provision "shell", inline: <<-SHELL 54 | docker swarm join --token $(cat /vagrant_data/swarm-worker-token) 10.0.0.10:2377 55 | SHELL 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test-cluster/commands.md: -------------------------------------------------------------------------------- 1 | # Up 2 | vagrant up 3 | 4 | # Run swarm-dashboard (image from Docker Hub) 5 | vagrant ssh manager1 6 | docker stack deploy -c /vagrant/compose-all.yml sd 7 | 8 | # Run swarm-dashboard (build locally) 9 | vagrant ssh manager1 10 | docker stack deploy -c /vagrant_parent/test-cluster/compose-metrics.yml sd 11 | docker-compose -f /vagrant_parent/test-cluster/compose-dashboard.yml up --build 12 | 13 | # Shutdown 14 | vagrant halt 15 | 16 | # Destroy 17 | vagrant destroy -f -------------------------------------------------------------------------------- /test-cluster/compose-all.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | swarm-dashboard: 5 | image: mohsenasm/swarm-dashboard:dev_stats 6 | volumes: 7 | - /var/run/docker.sock:/var/run/docker.sock 8 | ports: 9 | - 8080:8080 10 | environment: 11 | PORT: 8080 12 | ENABLE_AUTHENTICATION: "false" 13 | ENABLE_HTTPS: "false" 14 | NODE_EXPORTER_SERVICE_NAME_REGEX: "node-exporter" 15 | CADVISOR_SERVICE_NAME_REGEX: "cadvisor" 16 | deploy: 17 | placement: 18 | constraints: 19 | - node.role == manager 20 | 21 | node-exporter: 22 | image: quay.io/prometheus/node-exporter:v1.6.1 23 | volumes: 24 | - '/:/host:ro' 25 | command: 26 | - '--path.rootfs=/host' 27 | deploy: 28 | mode: global 29 | 30 | cadvisor: 31 | image: gcr.io/cadvisor/cadvisor 32 | volumes: 33 | - /:/rootfs:ro 34 | - /var/run:/var/run:rw 35 | - /sys:/sys:ro 36 | - /var/lib/docker/:/var/lib/docker:ro 37 | - /dev/disk/:/dev/disk:ro 38 | deploy: 39 | mode: global 40 | -------------------------------------------------------------------------------- /test-cluster/compose-dashboard.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | swarm-dashboard: 5 | build: .. 6 | volumes: 7 | - /var/run/docker.sock:/var/run/docker.sock 8 | ports: 9 | - 8080:8080 10 | environment: 11 | PORT: 8080 12 | ENABLE_AUTHENTICATION: "false" 13 | ENABLE_HTTPS: "false" 14 | NODE_EXPORTER_SERVICE_NAME_REGEX: "node-exporter" 15 | CADVISOR_SERVICE_NAME_REGEX: "cadvisor" 16 | networks: 17 | - monitoring_net 18 | 19 | networks: 20 | monitoring_net: 21 | external: 22 | name: sd_monitoring_net -------------------------------------------------------------------------------- /test-cluster/compose-metrics.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | node-exporter: 5 | image: quay.io/prometheus/node-exporter:v1.6.1 6 | volumes: 7 | - "/:/host:ro" 8 | command: 9 | - "--path.rootfs=/host" 10 | deploy: 11 | mode: global 12 | networks: 13 | - monitoring_net 14 | 15 | cadvisor: 16 | image: gcr.io/cadvisor/cadvisor 17 | volumes: 18 | - /:/rootfs:ro 19 | - /var/run:/var/run:rw 20 | - /sys:/sys:ro 21 | - /var/lib/docker/:/var/lib/docker:ro 22 | - /dev/disk/:/dev/disk:ro 23 | deploy: 24 | mode: global 25 | networks: 26 | - monitoring_net 27 | 28 | networks: 29 | monitoring_net: 30 | driver: overlay 31 | attachable: true 32 | -------------------------------------------------------------------------------- /test-cluster/wait-for-docker.sh: -------------------------------------------------------------------------------- 1 | until docker info > /dev/null 2 | do 3 | echo "waiting for docker info" 4 | sleep 1 5 | done -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@eslint-community/eslint-utils@^4.2.0": 6 | version "4.4.1" 7 | resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" 8 | integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== 9 | dependencies: 10 | eslint-visitor-keys "^3.4.3" 11 | 12 | "@eslint-community/regexpp@^4.12.1": 13 | version "4.12.1" 14 | resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" 15 | integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== 16 | 17 | "@eslint/config-array@^0.19.0": 18 | version "0.19.1" 19 | resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.1.tgz#734aaea2c40be22bbb1f2a9dac687c57a6a4c984" 20 | integrity sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA== 21 | dependencies: 22 | "@eslint/object-schema" "^2.1.5" 23 | debug "^4.3.1" 24 | minimatch "^3.1.2" 25 | 26 | "@eslint/core@^0.9.0": 27 | version "0.9.1" 28 | resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.9.1.tgz#31763847308ef6b7084a4505573ac9402c51f9d1" 29 | integrity sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q== 30 | dependencies: 31 | "@types/json-schema" "^7.0.15" 32 | 33 | "@eslint/eslintrc@^3.2.0": 34 | version "3.2.0" 35 | resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" 36 | integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== 37 | dependencies: 38 | ajv "^6.12.4" 39 | debug "^4.3.2" 40 | espree "^10.0.1" 41 | globals "^14.0.0" 42 | ignore "^5.2.0" 43 | import-fresh "^3.2.1" 44 | js-yaml "^4.1.0" 45 | minimatch "^3.1.2" 46 | strip-json-comments "^3.1.1" 47 | 48 | "@eslint/js@9.17.0": 49 | version "9.17.0" 50 | resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.17.0.tgz#1523e586791f80376a6f8398a3964455ecc651ec" 51 | integrity sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w== 52 | 53 | "@eslint/object-schema@^2.1.5": 54 | version "2.1.5" 55 | resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.5.tgz#8670a8f6258a2be5b2c620ff314a1d984c23eb2e" 56 | integrity sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ== 57 | 58 | "@eslint/plugin-kit@^0.2.3": 59 | version "0.2.4" 60 | resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz#2b78e7bb3755784bb13faa8932a1d994d6537792" 61 | integrity sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg== 62 | dependencies: 63 | levn "^0.4.1" 64 | 65 | "@humanfs/core@^0.19.1": 66 | version "0.19.1" 67 | resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" 68 | integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== 69 | 70 | "@humanfs/node@^0.16.6": 71 | version "0.16.6" 72 | resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" 73 | integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== 74 | dependencies: 75 | "@humanfs/core" "^0.19.1" 76 | "@humanwhocodes/retry" "^0.3.0" 77 | 78 | "@humanwhocodes/module-importer@^1.0.1": 79 | version "1.0.1" 80 | resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" 81 | integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== 82 | 83 | "@humanwhocodes/retry@^0.3.0": 84 | version "0.3.1" 85 | resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" 86 | integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== 87 | 88 | "@humanwhocodes/retry@^0.4.1": 89 | version "0.4.1" 90 | resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" 91 | integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== 92 | 93 | "@types/estree@^1.0.6": 94 | version "1.0.6" 95 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" 96 | integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== 97 | 98 | "@types/json-schema@^7.0.15": 99 | version "7.0.15" 100 | resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" 101 | integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== 102 | 103 | accepts@^2.0.0: 104 | version "2.0.0" 105 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" 106 | integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== 107 | dependencies: 108 | mime-types "^3.0.0" 109 | negotiator "^1.0.0" 110 | 111 | acorn-jsx@^5.3.2: 112 | version "5.3.2" 113 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" 114 | integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== 115 | 116 | acorn@^8.14.0: 117 | version "8.14.0" 118 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" 119 | integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== 120 | 121 | ajv@^6.12.4: 122 | version "6.12.6" 123 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 124 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 125 | dependencies: 126 | fast-deep-equal "^3.1.1" 127 | fast-json-stable-stringify "^2.0.0" 128 | json-schema-traverse "^0.4.1" 129 | uri-js "^4.2.2" 130 | 131 | ansi-styles@^4.1.0: 132 | version "4.3.0" 133 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 134 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 135 | dependencies: 136 | color-convert "^2.0.1" 137 | 138 | argparse@^2.0.1: 139 | version "2.0.1" 140 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 141 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 142 | 143 | array-flatten@3.0.0: 144 | version "3.0.0" 145 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-3.0.0.tgz#6428ca2ee52c7b823192ec600fa3ed2f157cd541" 146 | integrity sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA== 147 | 148 | babel-eslint-parser@7.13.10: 149 | version "7.13.10" 150 | resolved "https://registry.yarnpkg.com/babel-eslint-parser/-/babel-eslint-parser-7.13.10.tgz#b52f2c94d840bd1d53de2e5a152ab8bdfdb7ec6f" 151 | integrity sha512-Co1TrGEv1XiZ5Llga6RnjDehTLuU4FAD2I638iZIwuWqLx6kRVU3ck6C3nINQ+Xvz+Y1VgsBvt0uLdBSFQxpXQ== 152 | dependencies: 153 | eslint-scope "5.1.0" 154 | eslint-visitor-keys "^1.3.0" 155 | semver "condition:BABEL_8_BREAKING ? ^7.3.4 : ^6.3.0" 156 | 157 | balanced-match@^1.0.0: 158 | version "1.0.2" 159 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 160 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 161 | 162 | basic-auth@^2.0.1: 163 | version "2.0.1" 164 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" 165 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 166 | dependencies: 167 | safe-buffer "5.1.2" 168 | 169 | body-parser@^2.0.1: 170 | version "2.0.2" 171 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.0.2.tgz#52a90ca70bfafae03210b5b998e4ffcc3ecaecae" 172 | integrity sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ== 173 | dependencies: 174 | bytes "3.1.2" 175 | content-type "~1.0.5" 176 | debug "3.1.0" 177 | destroy "1.2.0" 178 | http-errors "2.0.0" 179 | iconv-lite "0.5.2" 180 | on-finished "2.4.1" 181 | qs "6.13.0" 182 | raw-body "^3.0.0" 183 | type-is "~1.6.18" 184 | 185 | brace-expansion@^1.1.7: 186 | version "1.1.11" 187 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 188 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 189 | dependencies: 190 | balanced-match "^1.0.0" 191 | concat-map "0.0.1" 192 | 193 | bytes@3.1.2: 194 | version "3.1.2" 195 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 196 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 197 | 198 | call-bind-apply-helpers@^1.0.1: 199 | version "1.0.1" 200 | resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" 201 | integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== 202 | dependencies: 203 | es-errors "^1.3.0" 204 | function-bind "^1.1.2" 205 | 206 | call-bound@^1.0.2: 207 | version "1.0.3" 208 | resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" 209 | integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== 210 | dependencies: 211 | call-bind-apply-helpers "^1.0.1" 212 | get-intrinsic "^1.2.6" 213 | 214 | callsites@^3.0.0: 215 | version "3.1.0" 216 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 217 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 218 | 219 | chalk@^4.0.0: 220 | version "4.1.2" 221 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 222 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 223 | dependencies: 224 | ansi-styles "^4.1.0" 225 | supports-color "^7.1.0" 226 | 227 | color-convert@^2.0.1: 228 | version "2.0.1" 229 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 230 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 231 | dependencies: 232 | color-name "~1.1.4" 233 | 234 | color-name@~1.1.4: 235 | version "1.1.4" 236 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 237 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 238 | 239 | concat-map@0.0.1: 240 | version "0.0.1" 241 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 242 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 243 | 244 | content-disposition@^1.0.0: 245 | version "1.0.0" 246 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" 247 | integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== 248 | dependencies: 249 | safe-buffer "5.2.1" 250 | 251 | content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: 252 | version "1.0.5" 253 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" 254 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 255 | 256 | cookie-signature@^1.2.1: 257 | version "1.2.2" 258 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" 259 | integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== 260 | 261 | cookie@0.7.1: 262 | version "0.7.1" 263 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" 264 | integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== 265 | 266 | cross-spawn@^7.0.6: 267 | version "7.0.6" 268 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" 269 | integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== 270 | dependencies: 271 | path-key "^3.1.0" 272 | shebang-command "^2.0.0" 273 | which "^2.0.1" 274 | 275 | debug@2.6.9: 276 | version "2.6.9" 277 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 278 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 279 | dependencies: 280 | ms "2.0.0" 281 | 282 | debug@3.1.0: 283 | version "3.1.0" 284 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 285 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 286 | dependencies: 287 | ms "2.0.0" 288 | 289 | debug@4.3.6: 290 | version "4.3.6" 291 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" 292 | integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== 293 | dependencies: 294 | ms "2.1.2" 295 | 296 | debug@^4.3.1, debug@^4.3.2, debug@^4.3.5: 297 | version "4.4.0" 298 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" 299 | integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== 300 | dependencies: 301 | ms "^2.1.3" 302 | 303 | deep-is@^0.1.3: 304 | version "0.1.4" 305 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" 306 | integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== 307 | 308 | depd@2.0.0: 309 | version "2.0.0" 310 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 311 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 312 | 313 | destroy@1.2.0, destroy@^1.2.0: 314 | version "1.2.0" 315 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 316 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 317 | 318 | dunder-proto@^1.0.0: 319 | version "1.0.1" 320 | resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" 321 | integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== 322 | dependencies: 323 | call-bind-apply-helpers "^1.0.1" 324 | es-errors "^1.3.0" 325 | gopd "^1.2.0" 326 | 327 | ee-first@1.1.1: 328 | version "1.1.1" 329 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 330 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 331 | 332 | encodeurl@^2.0.0, encodeurl@~2.0.0: 333 | version "2.0.0" 334 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" 335 | integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== 336 | 337 | encodeurl@~1.0.2: 338 | version "1.0.2" 339 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 340 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 341 | 342 | es-define-property@^1.0.1: 343 | version "1.0.1" 344 | resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" 345 | integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== 346 | 347 | es-errors@^1.3.0: 348 | version "1.3.0" 349 | resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" 350 | integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== 351 | 352 | es-object-atoms@^1.0.0: 353 | version "1.0.0" 354 | resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" 355 | integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== 356 | dependencies: 357 | es-errors "^1.3.0" 358 | 359 | escape-html@^1.0.3, escape-html@~1.0.3: 360 | version "1.0.3" 361 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 362 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 363 | 364 | escape-string-regexp@^4.0.0: 365 | version "4.0.0" 366 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 367 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 368 | 369 | eslint-scope@5.1.0: 370 | version "5.1.0" 371 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" 372 | integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== 373 | dependencies: 374 | esrecurse "^4.1.0" 375 | estraverse "^4.1.1" 376 | 377 | eslint-scope@^8.2.0: 378 | version "8.2.0" 379 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" 380 | integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== 381 | dependencies: 382 | esrecurse "^4.3.0" 383 | estraverse "^5.2.0" 384 | 385 | eslint-visitor-keys@^1.3.0: 386 | version "1.3.0" 387 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" 388 | integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== 389 | 390 | eslint-visitor-keys@^3.4.3: 391 | version "3.4.3" 392 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" 393 | integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== 394 | 395 | eslint-visitor-keys@^4.2.0: 396 | version "4.2.0" 397 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" 398 | integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== 399 | 400 | eslint@9.17.0: 401 | version "9.17.0" 402 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.17.0.tgz#faa1facb5dd042172fdc520106984b5c2421bb0c" 403 | integrity sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA== 404 | dependencies: 405 | "@eslint-community/eslint-utils" "^4.2.0" 406 | "@eslint-community/regexpp" "^4.12.1" 407 | "@eslint/config-array" "^0.19.0" 408 | "@eslint/core" "^0.9.0" 409 | "@eslint/eslintrc" "^3.2.0" 410 | "@eslint/js" "9.17.0" 411 | "@eslint/plugin-kit" "^0.2.3" 412 | "@humanfs/node" "^0.16.6" 413 | "@humanwhocodes/module-importer" "^1.0.1" 414 | "@humanwhocodes/retry" "^0.4.1" 415 | "@types/estree" "^1.0.6" 416 | "@types/json-schema" "^7.0.15" 417 | ajv "^6.12.4" 418 | chalk "^4.0.0" 419 | cross-spawn "^7.0.6" 420 | debug "^4.3.2" 421 | escape-string-regexp "^4.0.0" 422 | eslint-scope "^8.2.0" 423 | eslint-visitor-keys "^4.2.0" 424 | espree "^10.3.0" 425 | esquery "^1.5.0" 426 | esutils "^2.0.2" 427 | fast-deep-equal "^3.1.3" 428 | file-entry-cache "^8.0.0" 429 | find-up "^5.0.0" 430 | glob-parent "^6.0.2" 431 | ignore "^5.2.0" 432 | imurmurhash "^0.1.4" 433 | is-glob "^4.0.0" 434 | json-stable-stringify-without-jsonify "^1.0.1" 435 | lodash.merge "^4.6.2" 436 | minimatch "^3.1.2" 437 | natural-compare "^1.4.0" 438 | optionator "^0.9.3" 439 | 440 | espree@^10.0.1, espree@^10.3.0: 441 | version "10.3.0" 442 | resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" 443 | integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== 444 | dependencies: 445 | acorn "^8.14.0" 446 | acorn-jsx "^5.3.2" 447 | eslint-visitor-keys "^4.2.0" 448 | 449 | esquery@^1.5.0: 450 | version "1.6.0" 451 | resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" 452 | integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== 453 | dependencies: 454 | estraverse "^5.1.0" 455 | 456 | esrecurse@^4.1.0, esrecurse@^4.3.0: 457 | version "4.3.0" 458 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" 459 | integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 460 | dependencies: 461 | estraverse "^5.2.0" 462 | 463 | estraverse@^4.1.1: 464 | version "4.3.0" 465 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" 466 | integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 467 | 468 | estraverse@^5.1.0, estraverse@^5.2.0: 469 | version "5.3.0" 470 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" 471 | integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== 472 | 473 | esutils@^2.0.2: 474 | version "2.0.3" 475 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 476 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 477 | 478 | etag@^1.8.1, etag@~1.8.1: 479 | version "1.8.1" 480 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 481 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 482 | 483 | express-basic-auth@^1.2.1: 484 | version "1.2.1" 485 | resolved "https://registry.yarnpkg.com/express-basic-auth/-/express-basic-auth-1.2.1.tgz#d31241c03a915dd55db7e5285573049cfcc36381" 486 | integrity sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA== 487 | dependencies: 488 | basic-auth "^2.0.1" 489 | 490 | express@5.0.1: 491 | version "5.0.1" 492 | resolved "https://registry.yarnpkg.com/express/-/express-5.0.1.tgz#5d359a2550655be33124ecbc7400cd38436457e9" 493 | integrity sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ== 494 | dependencies: 495 | accepts "^2.0.0" 496 | body-parser "^2.0.1" 497 | content-disposition "^1.0.0" 498 | content-type "~1.0.4" 499 | cookie "0.7.1" 500 | cookie-signature "^1.2.1" 501 | debug "4.3.6" 502 | depd "2.0.0" 503 | encodeurl "~2.0.0" 504 | escape-html "~1.0.3" 505 | etag "~1.8.1" 506 | finalhandler "^2.0.0" 507 | fresh "2.0.0" 508 | http-errors "2.0.0" 509 | merge-descriptors "^2.0.0" 510 | methods "~1.1.2" 511 | mime-types "^3.0.0" 512 | on-finished "2.4.1" 513 | once "1.4.0" 514 | parseurl "~1.3.3" 515 | proxy-addr "~2.0.7" 516 | qs "6.13.0" 517 | range-parser "~1.2.1" 518 | router "^2.0.0" 519 | safe-buffer "5.2.1" 520 | send "^1.1.0" 521 | serve-static "^2.1.0" 522 | setprototypeof "1.2.0" 523 | statuses "2.0.1" 524 | type-is "^2.0.0" 525 | utils-merge "1.0.1" 526 | vary "~1.1.2" 527 | 528 | fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: 529 | version "3.1.3" 530 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 531 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 532 | 533 | fast-json-stable-stringify@^2.0.0: 534 | version "2.1.0" 535 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 536 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 537 | 538 | fast-levenshtein@^2.0.6: 539 | version "2.0.6" 540 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 541 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== 542 | 543 | file-entry-cache@^8.0.0: 544 | version "8.0.0" 545 | resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" 546 | integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== 547 | dependencies: 548 | flat-cache "^4.0.0" 549 | 550 | finalhandler@^2.0.0: 551 | version "2.0.0" 552 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.0.0.tgz#9d3c79156dfa798069db7de7dd53bc37546f564b" 553 | integrity sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ== 554 | dependencies: 555 | debug "2.6.9" 556 | encodeurl "~1.0.2" 557 | escape-html "~1.0.3" 558 | on-finished "2.4.1" 559 | parseurl "~1.3.3" 560 | statuses "2.0.1" 561 | unpipe "~1.0.0" 562 | 563 | find-up@^5.0.0: 564 | version "5.0.0" 565 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 566 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 567 | dependencies: 568 | locate-path "^6.0.0" 569 | path-exists "^4.0.0" 570 | 571 | flat-cache@^4.0.0: 572 | version "4.0.1" 573 | resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" 574 | integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== 575 | dependencies: 576 | flatted "^3.2.9" 577 | keyv "^4.5.4" 578 | 579 | flatted@^3.2.9: 580 | version "3.3.2" 581 | resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" 582 | integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== 583 | 584 | forwarded@0.2.0: 585 | version "0.2.0" 586 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 587 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 588 | 589 | fresh@2.0.0: 590 | version "2.0.0" 591 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" 592 | integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== 593 | 594 | fresh@^0.5.2: 595 | version "0.5.2" 596 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 597 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 598 | 599 | function-bind@^1.1.2: 600 | version "1.1.2" 601 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" 602 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 603 | 604 | get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: 605 | version "1.2.6" 606 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz#43dd3dd0e7b49b82b2dfcad10dc824bf7fc265d5" 607 | integrity sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA== 608 | dependencies: 609 | call-bind-apply-helpers "^1.0.1" 610 | dunder-proto "^1.0.0" 611 | es-define-property "^1.0.1" 612 | es-errors "^1.3.0" 613 | es-object-atoms "^1.0.0" 614 | function-bind "^1.1.2" 615 | gopd "^1.2.0" 616 | has-symbols "^1.1.0" 617 | hasown "^2.0.2" 618 | math-intrinsics "^1.0.0" 619 | 620 | glob-parent@^6.0.2: 621 | version "6.0.2" 622 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" 623 | integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== 624 | dependencies: 625 | is-glob "^4.0.3" 626 | 627 | globals@^14.0.0: 628 | version "14.0.0" 629 | resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" 630 | integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== 631 | 632 | gopd@^1.2.0: 633 | version "1.2.0" 634 | resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" 635 | integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== 636 | 637 | has-flag@^4.0.0: 638 | version "4.0.0" 639 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 640 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 641 | 642 | has-symbols@^1.1.0: 643 | version "1.1.0" 644 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" 645 | integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== 646 | 647 | hasown@^2.0.2: 648 | version "2.0.2" 649 | resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" 650 | integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== 651 | dependencies: 652 | function-bind "^1.1.2" 653 | 654 | http-errors@2.0.0, http-errors@^2.0.0: 655 | version "2.0.0" 656 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 657 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 658 | dependencies: 659 | depd "2.0.0" 660 | inherits "2.0.4" 661 | setprototypeof "1.2.0" 662 | statuses "2.0.1" 663 | toidentifier "1.0.1" 664 | 665 | iconv-lite@0.5.2: 666 | version "0.5.2" 667 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.2.tgz#af6d628dccfb463b7364d97f715e4b74b8c8c2b8" 668 | integrity sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag== 669 | dependencies: 670 | safer-buffer ">= 2.1.2 < 3" 671 | 672 | iconv-lite@0.6.3: 673 | version "0.6.3" 674 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" 675 | integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== 676 | dependencies: 677 | safer-buffer ">= 2.1.2 < 3.0.0" 678 | 679 | ignore@^5.2.0: 680 | version "5.3.2" 681 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" 682 | integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== 683 | 684 | import-fresh@^3.2.1: 685 | version "3.3.0" 686 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 687 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 688 | dependencies: 689 | parent-module "^1.0.0" 690 | resolve-from "^4.0.0" 691 | 692 | imurmurhash@^0.1.4: 693 | version "0.1.4" 694 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 695 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 696 | 697 | inherits@2.0.4: 698 | version "2.0.4" 699 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 700 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 701 | 702 | ipaddr.js@1.9.1: 703 | version "1.9.1" 704 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 705 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 706 | 707 | is-extglob@^2.1.1: 708 | version "2.1.1" 709 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 710 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 711 | 712 | is-glob@^4.0.0, is-glob@^4.0.3: 713 | version "4.0.3" 714 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 715 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 716 | dependencies: 717 | is-extglob "^2.1.1" 718 | 719 | is-promise@4.0.0: 720 | version "4.0.0" 721 | resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" 722 | integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== 723 | 724 | isexe@^2.0.0: 725 | version "2.0.0" 726 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 727 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 728 | 729 | js-yaml@^4.1.0: 730 | version "4.1.0" 731 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 732 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 733 | dependencies: 734 | argparse "^2.0.1" 735 | 736 | json-buffer@3.0.1: 737 | version "3.0.1" 738 | resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" 739 | integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== 740 | 741 | json-schema-traverse@^0.4.1: 742 | version "0.4.1" 743 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 744 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 745 | 746 | json-stable-stringify-without-jsonify@^1.0.1: 747 | version "1.0.1" 748 | resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 749 | integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== 750 | 751 | keyv@^4.5.4: 752 | version "4.5.4" 753 | resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" 754 | integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== 755 | dependencies: 756 | json-buffer "3.0.1" 757 | 758 | levn@^0.4.1: 759 | version "0.4.1" 760 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" 761 | integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== 762 | dependencies: 763 | prelude-ls "^1.2.1" 764 | type-check "~0.4.0" 765 | 766 | locate-path@^6.0.0: 767 | version "6.0.0" 768 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 769 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 770 | dependencies: 771 | p-locate "^5.0.0" 772 | 773 | lodash.merge@^4.6.2: 774 | version "4.6.2" 775 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" 776 | integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== 777 | 778 | math-intrinsics@^1.0.0: 779 | version "1.1.0" 780 | resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" 781 | integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== 782 | 783 | media-typer@0.3.0: 784 | version "0.3.0" 785 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 786 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 787 | 788 | media-typer@^1.1.0: 789 | version "1.1.0" 790 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" 791 | integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== 792 | 793 | merge-descriptors@^2.0.0: 794 | version "2.0.0" 795 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" 796 | integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== 797 | 798 | methods@~1.1.2: 799 | version "1.1.2" 800 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 801 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 802 | 803 | mime-db@1.52.0: 804 | version "1.52.0" 805 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 806 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 807 | 808 | mime-db@^1.53.0: 809 | version "1.53.0" 810 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" 811 | integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== 812 | 813 | mime-types@^2.1.35, mime-types@~2.1.24: 814 | version "2.1.35" 815 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 816 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 817 | dependencies: 818 | mime-db "1.52.0" 819 | 820 | mime-types@^3.0.0: 821 | version "3.0.0" 822 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.0.tgz#148453a900475522d095a445355c074cca4f5217" 823 | integrity sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w== 824 | dependencies: 825 | mime-db "^1.53.0" 826 | 827 | minimatch@^3.1.2: 828 | version "3.1.2" 829 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 830 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 831 | dependencies: 832 | brace-expansion "^1.1.7" 833 | 834 | moment@^2.29.4: 835 | version "2.30.1" 836 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" 837 | integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== 838 | 839 | ms@2.0.0: 840 | version "2.0.0" 841 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 842 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 843 | 844 | ms@2.1.2: 845 | version "2.1.2" 846 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 847 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 848 | 849 | ms@^2.1.3: 850 | version "2.1.3" 851 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 852 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 853 | 854 | natural-compare@^1.4.0: 855 | version "1.4.0" 856 | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 857 | integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== 858 | 859 | negotiator@^1.0.0: 860 | version "1.0.0" 861 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" 862 | integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== 863 | 864 | object-inspect@^1.13.3: 865 | version "1.13.3" 866 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" 867 | integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== 868 | 869 | on-finished@2.4.1, on-finished@^2.4.1: 870 | version "2.4.1" 871 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 872 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 873 | dependencies: 874 | ee-first "1.1.1" 875 | 876 | once@1.4.0: 877 | version "1.4.0" 878 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 879 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 880 | dependencies: 881 | wrappy "1" 882 | 883 | optionator@^0.9.3: 884 | version "0.9.4" 885 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" 886 | integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== 887 | dependencies: 888 | deep-is "^0.1.3" 889 | fast-levenshtein "^2.0.6" 890 | levn "^0.4.1" 891 | prelude-ls "^1.2.1" 892 | type-check "^0.4.0" 893 | word-wrap "^1.2.5" 894 | 895 | p-limit@^3.0.2: 896 | version "3.1.0" 897 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 898 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 899 | dependencies: 900 | yocto-queue "^0.1.0" 901 | 902 | p-locate@^5.0.0: 903 | version "5.0.0" 904 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 905 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 906 | dependencies: 907 | p-limit "^3.0.2" 908 | 909 | parent-module@^1.0.0: 910 | version "1.0.1" 911 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 912 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 913 | dependencies: 914 | callsites "^3.0.0" 915 | 916 | parse-prometheus-text-format@^1.1.1: 917 | version "1.1.1" 918 | resolved "https://registry.yarnpkg.com/parse-prometheus-text-format/-/parse-prometheus-text-format-1.1.1.tgz#498f68ebc391ffada81391ee81cf88325f857806" 919 | integrity sha512-dBlhYVACjRdSqLMFe4/Q1l/Gd3UmXm8ruvsTi7J6ul3ih45AkzkVpI5XHV4aZ37juGZW5+3dGU5lwk+QLM9XJA== 920 | dependencies: 921 | shallow-equal "^1.2.0" 922 | 923 | parseurl@^1.3.3, parseurl@~1.3.3: 924 | version "1.3.3" 925 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 926 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 927 | 928 | path-exists@^4.0.0: 929 | version "4.0.0" 930 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 931 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 932 | 933 | path-key@^3.1.0: 934 | version "3.1.1" 935 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 936 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 937 | 938 | path-to-regexp@^8.0.0: 939 | version "8.2.0" 940 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" 941 | integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== 942 | 943 | prelude-ls@^1.2.1: 944 | version "1.2.1" 945 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" 946 | integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 947 | 948 | proxy-addr@~2.0.7: 949 | version "2.0.7" 950 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 951 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 952 | dependencies: 953 | forwarded "0.2.0" 954 | ipaddr.js "1.9.1" 955 | 956 | punycode@^2.1.0: 957 | version "2.3.1" 958 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" 959 | integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== 960 | 961 | qs@6.13.0: 962 | version "6.13.0" 963 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" 964 | integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== 965 | dependencies: 966 | side-channel "^1.0.6" 967 | 968 | ramda@0.30.1: 969 | version "0.30.1" 970 | resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.1.tgz#7108ac95673062b060025052cd5143ae8fc605bf" 971 | integrity sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw== 972 | 973 | range-parser@^1.2.1, range-parser@~1.2.1: 974 | version "1.2.1" 975 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 976 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 977 | 978 | raw-body@^3.0.0: 979 | version "3.0.0" 980 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" 981 | integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== 982 | dependencies: 983 | bytes "3.1.2" 984 | http-errors "2.0.0" 985 | iconv-lite "0.6.3" 986 | unpipe "1.0.0" 987 | 988 | resolve-from@^4.0.0: 989 | version "4.0.0" 990 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 991 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 992 | 993 | router@^2.0.0: 994 | version "2.0.0" 995 | resolved "https://registry.yarnpkg.com/router/-/router-2.0.0.tgz#8692720b95de83876870d7bc638dd3c7e1ae8a27" 996 | integrity sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ== 997 | dependencies: 998 | array-flatten "3.0.0" 999 | is-promise "4.0.0" 1000 | methods "~1.1.2" 1001 | parseurl "~1.3.3" 1002 | path-to-regexp "^8.0.0" 1003 | setprototypeof "1.2.0" 1004 | utils-merge "1.0.1" 1005 | 1006 | safe-buffer@5.1.2: 1007 | version "5.1.2" 1008 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 1009 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 1010 | 1011 | safe-buffer@5.2.1: 1012 | version "5.2.1" 1013 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 1014 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 1015 | 1016 | "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": 1017 | version "2.1.2" 1018 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 1019 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 1020 | 1021 | "semver@condition:BABEL_8_BREAKING ? ^7.3.4 : ^6.3.0": 1022 | version "7.6.3" 1023 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" 1024 | integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== 1025 | 1026 | send@^1.0.0, send@^1.1.0: 1027 | version "1.1.0" 1028 | resolved "https://registry.yarnpkg.com/send/-/send-1.1.0.tgz#4efe6ff3bb2139b0e5b2648d8b18d4dec48fc9c5" 1029 | integrity sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA== 1030 | dependencies: 1031 | debug "^4.3.5" 1032 | destroy "^1.2.0" 1033 | encodeurl "^2.0.0" 1034 | escape-html "^1.0.3" 1035 | etag "^1.8.1" 1036 | fresh "^0.5.2" 1037 | http-errors "^2.0.0" 1038 | mime-types "^2.1.35" 1039 | ms "^2.1.3" 1040 | on-finished "^2.4.1" 1041 | range-parser "^1.2.1" 1042 | statuses "^2.0.1" 1043 | 1044 | serve-static@^2.1.0: 1045 | version "2.1.0" 1046 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.1.0.tgz#1b4eacbe93006b79054faa4d6d0a501d7f0e84e2" 1047 | integrity sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA== 1048 | dependencies: 1049 | encodeurl "^2.0.0" 1050 | escape-html "^1.0.3" 1051 | parseurl "^1.3.3" 1052 | send "^1.0.0" 1053 | 1054 | setprototypeof@1.2.0: 1055 | version "1.2.0" 1056 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 1057 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 1058 | 1059 | shallow-equal@^1.2.0: 1060 | version "1.2.1" 1061 | resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" 1062 | integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== 1063 | 1064 | shebang-command@^2.0.0: 1065 | version "2.0.0" 1066 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 1067 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 1068 | dependencies: 1069 | shebang-regex "^3.0.0" 1070 | 1071 | shebang-regex@^3.0.0: 1072 | version "3.0.0" 1073 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 1074 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 1075 | 1076 | side-channel-list@^1.0.0: 1077 | version "1.0.0" 1078 | resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" 1079 | integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== 1080 | dependencies: 1081 | es-errors "^1.3.0" 1082 | object-inspect "^1.13.3" 1083 | 1084 | side-channel-map@^1.0.1: 1085 | version "1.0.1" 1086 | resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" 1087 | integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== 1088 | dependencies: 1089 | call-bound "^1.0.2" 1090 | es-errors "^1.3.0" 1091 | get-intrinsic "^1.2.5" 1092 | object-inspect "^1.13.3" 1093 | 1094 | side-channel-weakmap@^1.0.2: 1095 | version "1.0.2" 1096 | resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" 1097 | integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== 1098 | dependencies: 1099 | call-bound "^1.0.2" 1100 | es-errors "^1.3.0" 1101 | get-intrinsic "^1.2.5" 1102 | object-inspect "^1.13.3" 1103 | side-channel-map "^1.0.1" 1104 | 1105 | side-channel@^1.0.6: 1106 | version "1.1.0" 1107 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" 1108 | integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== 1109 | dependencies: 1110 | es-errors "^1.3.0" 1111 | object-inspect "^1.13.3" 1112 | side-channel-list "^1.0.0" 1113 | side-channel-map "^1.0.1" 1114 | side-channel-weakmap "^1.0.2" 1115 | 1116 | statuses@2.0.1, statuses@^2.0.1: 1117 | version "2.0.1" 1118 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 1119 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 1120 | 1121 | strip-json-comments@^3.1.1: 1122 | version "3.1.1" 1123 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 1124 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 1125 | 1126 | supports-color@^7.1.0: 1127 | version "7.2.0" 1128 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 1129 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 1130 | dependencies: 1131 | has-flag "^4.0.0" 1132 | 1133 | toidentifier@1.0.1: 1134 | version "1.0.1" 1135 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 1136 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 1137 | 1138 | type-check@^0.4.0, type-check@~0.4.0: 1139 | version "0.4.0" 1140 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" 1141 | integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== 1142 | dependencies: 1143 | prelude-ls "^1.2.1" 1144 | 1145 | type-is@^2.0.0: 1146 | version "2.0.0" 1147 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.0.tgz#7d249c2e2af716665cc149575dadb8b3858653af" 1148 | integrity sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw== 1149 | dependencies: 1150 | content-type "^1.0.5" 1151 | media-typer "^1.1.0" 1152 | mime-types "^3.0.0" 1153 | 1154 | type-is@~1.6.18: 1155 | version "1.6.18" 1156 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1157 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1158 | dependencies: 1159 | media-typer "0.3.0" 1160 | mime-types "~2.1.24" 1161 | 1162 | unpipe@1.0.0, unpipe@~1.0.0: 1163 | version "1.0.0" 1164 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1165 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 1166 | 1167 | uri-js@^4.2.2: 1168 | version "4.4.1" 1169 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 1170 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 1171 | dependencies: 1172 | punycode "^2.1.0" 1173 | 1174 | utils-merge@1.0.1: 1175 | version "1.0.1" 1176 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1177 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 1178 | 1179 | uuid@11.0.3: 1180 | version "11.0.3" 1181 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" 1182 | integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== 1183 | 1184 | vary@~1.1.2: 1185 | version "1.1.2" 1186 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1187 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 1188 | 1189 | which@^2.0.1: 1190 | version "2.0.2" 1191 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 1192 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 1193 | dependencies: 1194 | isexe "^2.0.0" 1195 | 1196 | word-wrap@^1.2.5: 1197 | version "1.2.5" 1198 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" 1199 | integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== 1200 | 1201 | wrappy@1: 1202 | version "1.0.2" 1203 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1204 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 1205 | 1206 | ws@^8.17.1: 1207 | version "8.18.0" 1208 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" 1209 | integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== 1210 | 1211 | yocto-queue@^0.1.0: 1212 | version "0.1.0" 1213 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 1214 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 1215 | --------------------------------------------------------------------------------