├── .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 | [](https://github.com/mohsenasm/swarm-dashboard/actions/workflows/main.yml)
6 |
7 | 
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 | [](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 |
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 |
--------------------------------------------------------------------------------