├── .dockerignore ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── docker-builds.yml ├── .gitignore ├── .gitlab-ci.yml ├── .markdownlint.json ├── CONFIGURATION.md ├── Dockerfile ├── Dockerfile.unprivileged ├── LICENSE ├── README.md ├── THEMING.md ├── babel.config.js ├── charts └── gitlab-monitor │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ └── service.yaml │ └── values.yaml ├── docker └── docker-compose.yml ├── index.html ├── index.template.html ├── logo.svg ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── manifest.json ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png └── mstile-70x70.png ├── scripts └── wrapper.sh ├── src ├── Config.js ├── GitLabApi.js ├── assets │ ├── backdrop.svg │ └── icons.svg ├── components │ ├── app.vue │ ├── gitlab-icon.vue │ ├── job-view.vue │ ├── pipeline-view.vue │ ├── project-card.vue │ ├── runner-status.vue │ ├── stage-view.vue │ └── test-report.vue ├── config.default.json ├── config.template.json ├── main.js ├── themes │ ├── nord-dark.theme.scss │ └── nord-light.theme.scss └── util.js ├── vue.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .git 3 | .gitignore 4 | .gitlab-ci.yml 5 | .editorconfig 6 | .markdownlint.json 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: timoschwarzer 2 | -------------------------------------------------------------------------------- /.github/workflows/docker-builds.yml: -------------------------------------------------------------------------------- 1 | name: docker-build-push 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | tags: 8 | - '*' 9 | 10 | pull_request: 11 | branches: 12 | - 'main' 13 | 14 | workflow_dispatch: 15 | 16 | env: 17 | IMAGE_NAME: "gitlab-monitor" 18 | 19 | jobs: 20 | docker: 21 | runs-on: ubuntu-latest 22 | steps: 23 | 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: Prepare 28 | id: prep 29 | run: | 30 | BASE_DIR=. 31 | IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME 32 | 33 | # Change all uppercase to lowercase 34 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') 35 | 36 | # Strip git ref prefix from version 37 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 38 | 39 | # Strip "v" prefix from tag name 40 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') 41 | 42 | # Use Docker `latest` tag convention 43 | [ "$VERSION" == "main" ] && VERSION=latest 44 | 45 | VERSION=$VERSION 46 | IMAGE_ID=$IMAGE_ID 47 | echo ::set-output name=version::${VERSION} 48 | echo ::set-output name=image_id::${IMAGE_ID} 49 | echo ::set-output name=base_dir::${BASE_DIR} 50 | 51 | - name: Set up QEMU 52 | uses: docker/setup-qemu-action@v1 53 | with: 54 | platforms: all 55 | 56 | - name: Set up Docker Buildx 57 | id: buildx 58 | uses: docker/setup-buildx-action@v1 59 | with: 60 | install: true 61 | version: latest 62 | 63 | - name: Log into registry 64 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 65 | 66 | - name: Build and Push 67 | uses: docker/build-push-action@v2 68 | with: 69 | build-args: VERSION=${{ steps.prep.outputs.version }} 70 | context: ${{ steps.prep.outputs.base_dir }}/ 71 | file: ${{ steps.prep.outputs.base_dir }}/Dockerfile 72 | platforms: linux/amd64,linux/arm64 73 | push: true 74 | tags: | 75 | ${{ steps.prep.outputs.image_id }}:${{ steps.prep.outputs.version }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | public/config.json 5 | npm-debug.log 6 | yarn-error.log 7 | 8 | # Editor directories and files 9 | *.todo 10 | .idea 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | *.sublime-* 16 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:12-alpine 2 | 3 | cache: 4 | paths: 5 | - node_modules/ 6 | 7 | stages: 8 | - build 9 | 10 | build: 11 | stage: build 12 | only: 13 | - master 14 | artifacts: 15 | paths: 16 | - dist 17 | script: 18 | - yarn install --check-files --non-interactive 19 | - yarn run build 20 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD013": false, 4 | "MD026": { "punctuation": ".,;:?" } 5 | } -------------------------------------------------------------------------------- /CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | GitLab Monitor is configured with a YAML-encoded configuration file. 4 | 5 | Your configuration overrides the default configuration, which can be here: [/src/config.default.json](./src/config.default.json) 6 | 7 | The configuration can be loaded statically or dynamically. 8 | 9 | 10 | ## Dynamic configuration 11 | 12 | - Start the server in dev or production mode (see [README.md](./README.md)). 13 | - Open the application URL 14 | - On the home page, you'll be asked to enter your configuration (basically you gitlab API endpoint and your private token) 15 | - Once configured, to change the existing configuration, hover the bottom left corner of the viewport! 16 | 17 | 18 | ## Static configuration 19 | 20 | To add your configuration, copy [/src/config.template.json](./src/config.template.json) file into `/public/config.json` and update value. 21 | 22 | ```json 23 | { 24 | "gitlabApi": "https://gitlab.example.com/api/v4", 25 | "privateToken": "ABCDEF1234" 26 | } 27 | ``` 28 | 29 | Your can also use "Headless configuration" or "Environment variable configuration" ask detailed below: 30 | 31 | ### Headless configuration 32 | If you can't access the browser directly for some reason, you can pass 33 | your configuration YAML encoded with base64url as `rawConfig` query parameter. 34 | 35 | ### Injecting configuration when starting the container 36 | You can also inject your configuration at runtime by setting an environment variable, `GITLAB_MONITOR_CONFIG`. 37 | 38 | ## Configuration options 39 | 40 | Here you'll find documentation about each configration option: 41 | 42 | ```yaml 43 | # Required 44 | gitlabApi: https://gitlab.example.com/api/v4 45 | 46 | # Required 47 | # Can be generated here: https://gitlab.example.com/profile/personal_access_tokens 48 | # At least api_read scope is needed 49 | privateToken: ABCDEF123456 50 | 51 | # In hours. Projects with last activity older than this age won't be displayed. 52 | # If set to 0, no filter will be applied 53 | maxAge: 168 54 | 55 | # How many projects will be fetched from GitLab. 56 | # If set to greater than 100, then all available projects will be retrieved (in batches of 100) 57 | fetchCount: 20 58 | 59 | # Show only projects with recent pipelines 60 | pipelinesOnly: false 61 | 62 | # Whether to include archived projects 63 | includeArchived: false 64 | 65 | # Zooms the dashboard to fill the screen with all displayed projects. 66 | # Not compatible with all browsers! See https://caniuse.com/#feat=css-zoom 67 | autoZoom: false 68 | 69 | # Whether to show pipeline IDs or not 70 | showPipelineIds: true 71 | 72 | # Only show project for specific job statuses. 73 | # Valid values are: 'created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped' or 'manual'. 74 | showProjectOnlyOn: 75 | - canceled 76 | - failed 77 | - pending 78 | - running 79 | - skipped 80 | - manual 81 | 82 | # Control how to show job names and icons 83 | # Can be: 'icon', 'name', 'iconAndName' 84 | showJobs: icon 85 | 86 | # Only show job names for specific job statuses. 87 | # Valid values are: 'created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped' or 'manual'. 88 | # This is an example that shows job names only for jobs with a non-success status: 89 | showJobsNameOnlyOn: 90 | - canceled 91 | - failed 92 | - pending 93 | - running 94 | - skipped 95 | - manual 96 | 97 | # Whether to show jobs that was restarted 98 | showRestartedJobs: true 99 | 100 | # Whether to show stages names 101 | showStagesNames: false 102 | 103 | # Whether to show pipeline durations or not 104 | showDurations: true 105 | 106 | # Whether to show the pipelines test coverage if available 107 | showCoverage: false 108 | 109 | # Whether to show the pipelines test report if available 110 | showTestReport: true 111 | 112 | # Whether to show the user that invoked the pipeline or not 113 | showUsers: false 114 | 115 | # Whether to show a button to rerun pipelines 116 | showRerunButton: false 117 | 118 | # The page title, or null to hide 119 | title: null 120 | 121 | # Project display order. Possible values: lastActivity, created, name, nameWithNamespace 122 | # Note that this only changes the displayed order and not the order in which they will be fetched. 123 | orderBy: lastActivity 124 | 125 | # Display projects in descending order 126 | orderByDesc: false 127 | 128 | # Multiply all polling intervals by this amount 129 | # (e.g. 0.5 will make gitlab-monitor poll twice as often) 130 | pollingIntervalMultiplier: 1.0 131 | 132 | # Disable to prevent refreshes while the browser tab is inactive. 133 | backgroundRefresh: true 134 | 135 | # Limit projects by visibility 136 | # Can be: 'any', 'public', 'internal' or 'private' 137 | projectVisibility: any 138 | 139 | # Limit by projects that the current user is a member of 140 | membership: false 141 | 142 | # Whether to show project badges or not 143 | badges: true 144 | 145 | # Whether to show the amount and availability of runners 146 | showRunnerStatus: true 147 | 148 | # Filter projects 149 | # The filter is applied to the path with namespace 150 | # (e.g. 'my-group/my-project' 151 | filter: 152 | 153 | # Include projects that match this RegExp 154 | include: .* 155 | 156 | # Include projects that have tags matching this RegExp 157 | includeTags: .* 158 | 159 | # Exclude projects of included projects that match this RegExp 160 | exclude: null 161 | 162 | # Exclude projects of included projects that have tags matching this RegExp 163 | excludeTags: null 164 | 165 | # Exclude projects of included projects that don't have any tags 166 | excludeUntagged: false 167 | 168 | # Configure projects 169 | projectConfig: 170 | 171 | # The asterisk selects all projects that 172 | # don't have their own configuration 173 | '*': 174 | 175 | # Include branches that match this RegExp 176 | include: .* 177 | 178 | # Exclude branches of included branches that match this RegExp 179 | exclude: null 180 | 181 | # Override default branch (used for the card background color) 182 | # If null, it uses the default branch of the project 183 | default: null 184 | 185 | # Whether to show pipelines of merged branches 186 | showMerged: true 187 | 188 | # Whether to show pipelines of tags 189 | showTags: true 190 | 191 | # Whether to show only the latest tag 192 | showLatestTagOnly: false 193 | 194 | # Whether to show detached pipelines of merge requests 195 | showDetached: false 196 | 197 | # Whether to show the labels of the merge request for detached pipelines 198 | showLabels: true 199 | 200 | # Minimum number of pipelines to display for this filter 201 | historyCount: 1 202 | 203 | # Hide skipped pipelines 204 | hideSkippedPipelines: false 205 | 206 | # Hide successful pipelines 207 | hideSuccessfulPipelines: false 208 | 209 | soundAlerts: 210 | # If set to a non-null value, sound alerts will be enabled. 211 | # Replace null with URL to sound file. 212 | # IMPORTANT: Due to some browsers blocking autoplaying audio 213 | # you may have to enable audio autoplay first! 214 | # Firefox: https://support.mozilla.org/en-US/kb/block-autoplay 215 | soundUrl: null 216 | 217 | # Play alert sounds for all branches matching this regex 218 | include: .* 219 | 220 | # Play alert sounds for all included branches except for branches 221 | # matching this regex. Set to null to exclude none. 222 | exclude: null 223 | 224 | # Specific per-project filters 225 | my-project/my-group": 226 | # see above... 227 | 228 | # Search projects from given API route. 229 | # Allowed values "groups" and "users" 230 | projectScope: null 231 | 232 | # ID or IDs to use in query with "projectScope" parameter 233 | # For example 123 with "projectScope": "groups" would query /groups/123/projects 234 | # Specify an array if you with to include multiple scope IDs. 235 | projectScopeId: null 236 | 237 | # If projectScope is "groups" you might be interested in using includeSubgroups to include 238 | # projects from all subgroups within the group specified in projectScopeId. 239 | includeSubgroups: false 240 | 241 | # Predefined theme to use. If null, the default theme will be used. 242 | # Available themes: nord-dark, nord-light 243 | # You can override styles in the settings or submit your own theme here: https://git.io/JUOFb 244 | theme: null 245 | ``` 246 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | FROM node:12-alpine as build 3 | 4 | WORKDIR /usr/src/app 5 | COPY . ./ 6 | RUN yarn install \ 7 | && yarn build 8 | 9 | # Stage 2 10 | FROM nginx:1.17-alpine 11 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html 12 | COPY scripts/wrapper.sh /wrapper.sh 13 | CMD ["/wrapper.sh"] 14 | EXPOSE 80 15 | -------------------------------------------------------------------------------- /Dockerfile.unprivileged: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | FROM node:11-alpine as build 3 | 4 | WORKDIR /usr/src/app 5 | COPY . ./ 6 | RUN yarn install \ 7 | && yarn build 8 | 9 | # Stage 2 10 | FROM nginxinc/nginx-unprivileged:1.15-alpine 11 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html 12 | COPY scripts/wrapper.sh /wrapper.sh 13 | CMD ["/wrapper.sh"] 14 | EXPOSE 8080 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Timo Schwarzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

An abstract representation of an eye GitLab Monitor

2 | 3 | > A browser-based monitor dashboard for GitLab CI 4 | 5 | ## Use & Download 6 | 7 | ### Hosted version 8 | 9 | If you don't want to setup your own server, you can always 10 | use the latest version of GitLab Monitor I upload here: 11 | 12 | 13 | 14 | Don't worry, I don't save access tokens or anything else. 15 | Additionally, this version has a manifest.json attached which 16 | makes it easy to pin it to your Android home screen and open 17 | it as a full screen app. 18 | 19 | [**Support me on Patreon**](https://www.patreon.com/timoschwarzer) 20 | 21 | ### Docker 22 | 23 | [![dockeri.co](https://dockeri.co/image/timoschwarzer/gitlab-monitor)](https://hub.docker.com/r/timoschwarzer/gitlab-monitor) 24 | 25 | There's an official docker image available on [Dockerhub](https://hub.docker.com/r/timoschwarzer/gitlab-monitor/): 26 | ``` 27 | docker pull timoschwarzer/gitlab-monitor 28 | ``` 29 | 30 | ### Host it yourself 31 | 32 | [Go to releases](https://github.com/timoschwarzer/gitlab-monitor/releases) 33 | 34 | ## Screenshots 35 | 36 | ![Screenshot 1](/../resources/screenshots/screenshot1.png?raw=true) 37 | ![Screenshot 2](/../resources/screenshots/screenshot2.png?raw=true) 38 | 39 | ## Build Setup 40 | 41 | ``` bash 42 | # install dependencies 43 | yarn install 44 | 45 | # serve with hot reload at localhost:8080 46 | yarn run dev 47 | 48 | # build for production with minification 49 | yarn run build 50 | ``` 51 | 52 | ## Configuration 53 | See [configuration](./CONFIGURATION.md). 54 | 55 | ## Used libraries 56 | 57 | - [Vue](https://vuejs.org) 58 | - [vue-timeago](https://github.com/egoist/vue-timeago) 59 | - [vue-octicon](https://github.com/Justineo/vue-octicon) 60 | - [GitLab SVG icons](https://gitlab.com/gitlab-org/gitlab-svgs) 61 | 62 | ### Visit my [website](https://timoschwarzer.com)! 63 | -------------------------------------------------------------------------------- /THEMING.md: -------------------------------------------------------------------------------- 1 | # Theming 2 | 3 | You can submit your own themes as a pull request to make them available to all users. 4 | 5 | ## Creating a Theme 6 | 7 | To create a theme simply create a `my-theme.theme.scss` file under `src/themes/` 8 | and add a global class selector named after your theme. 9 | 10 | ```scss 11 | /* src/themes/my-theme.theme.scss */ 12 | 13 | .my-theme { 14 | // Your styles here 15 | } 16 | ``` 17 | 18 | Enable your theme in your configuration to see your changes: 19 | 20 | ```yaml 21 | theme: my-theme 22 | ``` 23 | 24 | ## Variables 25 | 26 | Themes use [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) for variables. 27 | 28 | There are quite a few variables out there to make theme development easier. 29 | Take a look at existing themes for a list of variables or check out 30 | your browser's dev tools. 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: A Helm chart for Kubernetes 3 | name: gitlab-monitor 4 | version: v0.2.0 5 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/README.md: -------------------------------------------------------------------------------- 1 | # Gitlab Monitor Helm Chart 2 | 3 | ## Prerequisites 4 | 5 | 1. Have a running [Kubernetes](https://kubernetes.io/docs/setup/) environment 6 | 2. Setup [Helm](https://github.com/kubernetes/helm) 7 | 8 | ## Install 9 | 10 | ### From File 11 | Clone gitlab-monitor and change directory 12 | ```bash 13 | $ git clone https://github.com/timoschwarzer/gitlab-monitor 14 | $ cd gitlab-monitor/charts/gitlab-monitor/ 15 | ``` 16 | Helm install monitor 17 | > note name and namespace can be customized. 18 | ```bash 19 | $ helm install . -n gitlab-monitor --namespace default 20 | ``` 21 | 22 | ## Configure 23 | 24 | Edit the `values.yaml` and set the config yaml string accordingly. 25 | 26 | ```yaml 27 | config: | 28 | { 29 | "gitlabApi": "https://gitlab.example.com/api/v4", 30 | "privateToken": "ABCDEF123456", 31 | "maxAge": 168, 32 | "fetchCount": 20, 33 | "pipelinesOnly": false, 34 | "includeArchived": false, 35 | "autoZoom": false, 36 | "showPipelineIds": true, 37 | "showJobs": "iconAndName", 38 | "showDurations": true, 39 | "showUsers": false, 40 | "projectVisibility": "any", 41 | "linkToFailureSound": null , 42 | "title": null, 43 | "pollingIntervalMultiplier": 10, 44 | "filter": { 45 | "include": ".*", 46 | "includeTags": ".*", 47 | "exclude": null, 48 | "excludeTags": null 49 | }, 50 | "projectFilter": { 51 | "*": { 52 | "include": ".*", 53 | "exclude": null, 54 | "default": null, 55 | "showMerged": false, 56 | "showTags": true, 57 | "showLatestTagOnly": false, 58 | "showDetached": false, 59 | "showLabels": true, 60 | "maxPipelines": 10, 61 | "notifyFailureOn": null 62 | } 63 | } 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 2 | {{- if contains "NodePort" .Values.service.type }} 3 | Get the application URL by running these commands: 4 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "fullname" . }}) 5 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 6 | echo http://$NODE_IP:$NODE_PORT/login 7 | {{- else if contains "LoadBalancer" .Values.service.type }} 8 | Get the application URL by running these commands: 9 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 10 | You can watch the status of by running 'kubectl get svc -w {{ template "fullname" . }}' 11 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 12 | echo http://$SERVICE_IP:{{ .Values.service.externalPort }} 13 | {{- else }} 14 | http://{{ .Values.ingress.domain }} to access your application 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | draft: {{ default "draft-app" .Values.draft }} 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | template: 11 | metadata: 12 | annotations: 13 | buildID: {{ .Values.buildID }} 14 | labels: 15 | draft: {{ default "draft-app" .Values.draft }} 16 | app: {{ template "fullname" . }} 17 | spec: 18 | containers: 19 | - name: {{ .Chart.Name }} 20 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 21 | imagePullPolicy: {{ .Values.image.pullPolicy }} 22 | env: 23 | - name: GITLAB_MONITOR_CONFIG 24 | value: | {{ .Values.config | trim | nindent 12 }} 25 | ports: 26 | - containerPort: {{ .Values.service.internalPort }} 27 | resources: 28 | {{ toYaml .Values.resources | indent 12 }} 29 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: {{ template "fullname" . }} 6 | labels: 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 8 | spec: 9 | rules: 10 | - host: {{ .Values.ingress.domain }} 11 | http: 12 | paths: 13 | - path: / 14 | backend: 15 | serviceName: {{ template "fullname" . }} 16 | servicePort: {{ .Values.service.externalPort }} 17 | {{- if .Values.ingress.tls }} 18 | tls: 19 | {{ toYaml .Values.ingress.tls | indent 4 }} 20 | {{- end }} 21 | {{- end -}} 22 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.externalPort }} 11 | targetPort: {{ .Values.service.internalPort }} 12 | {{- if eq .Values.service.type "LoadBalancer"}} 13 | nodePort: {{ .Values.service.nodePort }} 14 | {{- end}} 15 | protocol: TCP 16 | name: {{ .Values.service.name }} 17 | selector: 18 | app: {{ template "fullname" . }} 19 | -------------------------------------------------------------------------------- /charts/gitlab-monitor/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for node. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | replicaCount: 1 5 | image: 6 | pullPolicy: Always 7 | repository: timoschwarzer/gitlab-monitor 8 | tag: latest 9 | service: 10 | name: node 11 | type: LoadBalancer 12 | externalPort: 80 13 | internalPort: 80 14 | nodePort: 31148 15 | resources: 16 | limits: 17 | cpu: 100m 18 | memory: 128Mi 19 | requests: 20 | cpu: 100m 21 | memory: 128Mi 22 | ingress: 23 | enabled: false 24 | domain: gitlab-monitor.example.com 25 | tls: {} 26 | config: | 27 | { 28 | "gitlabApi": "https://gitlab.example.com/api/v4", 29 | "privateToken": "ABCDEF123456", 30 | "maxAge": 168, 31 | "fetchCount": 20, 32 | "pipelinesOnly": false, 33 | "includeArchived": false, 34 | "autoZoom": false, 35 | "showPipelineIds": true, 36 | "showJobs": "iconAndName", 37 | "showDurations": true, 38 | "showUsers": false, 39 | "projectVisibility": "any", 40 | "linkToFailureSound": null , 41 | "title": null, 42 | "pollingIntervalMultiplier": 10, 43 | "filter": { 44 | "include": ".*", 45 | "includeTags": ".*", 46 | "exclude": null, 47 | "excludeTags": null 48 | }, 49 | "projectFilter": { 50 | "*": { 51 | "include": ".*", 52 | "exclude": null, 53 | "default": null, 54 | "showMerged": false, 55 | "showTags": true, 56 | "showLatestTagOnly": false, 57 | "showDetached": false, 58 | "showLabels": true, 59 | "maxPipelines": 10, 60 | "notifyFailureOn": null 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | web: 5 | external: true 6 | 7 | services: 8 | ui: 9 | image: timoschwarzer/gitlab-monitor 10 | networks: 11 | - web 12 | expose: 13 | - 80 14 | restart: always 15 | labels: 16 | - "traefik.enable=true" 17 | - "traefik.frontend.rule=Host:gitlab-monitor.timoschwarzer.com" 18 | - "traefik.port=80" 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GitLab Monitor 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GitLab Monitor 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitlab-monitor", 3 | "description": "A browser-based monitor dashboard for GitLab CI", 4 | "version": "1.0.0", 5 | "author": "Timo Schwarzer ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "vue-cli-service serve", 10 | "build": "vue-cli-service build", 11 | "serve": "http-server ./dist" 12 | }, 13 | "dependencies": { 14 | "@openfonts/roboto-condensed_all": "^1.44.1", 15 | "@openfonts/roboto_all": "^1.44.1", 16 | "axios": "^0.21.1", 17 | "base64url": "^3.0.0", 18 | "core-js": "^3.3.2", 19 | "date-fns": "^1.30.1", 20 | "deepmerge": "^2.2.1", 21 | "http-server": "^0.11.1", 22 | "monaco-editor-webpack-plugin": "1.9.0", 23 | "nord": "^0.2.1", 24 | "parse-link-header": "^1.0.1", 25 | "query-string": "^6.2.0", 26 | "regenerator-runtime": "^0.13.3", 27 | "visibilityjs": "^2.0.2", 28 | "vue": "^2.6.10", 29 | "vue-monaco": "1.2.1", 30 | "vue-octicon": "^2.1.1", 31 | "vue-timeago": "^5.0.0", 32 | "yaml": "^1.6.0" 33 | }, 34 | "browserslist": [ 35 | "> 1%", 36 | "last 2 versions", 37 | "not ie <= 8" 38 | ], 39 | "devDependencies": { 40 | "@vue/cli-plugin-babel": "^4.0.4", 41 | "@vue/cli-service": "^4.0.4", 42 | "file-loader": "^3.0.1", 43 | "sass": "^1.23.0", 44 | "sass-loader": "^8.0.0", 45 | "style-loader": "^1.2.1", 46 | "vue-template-compiler": "^2.6.10" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #603cba 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GitLab Monitor 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitLab Monitor", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/android-chrome-512x512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#571d54", 16 | "background_color": "#571d54", 17 | "display": "standalone" 18 | } 19 | -------------------------------------------------------------------------------- /public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/mstile-144x144.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/mstile-310x150.png -------------------------------------------------------------------------------- /public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/mstile-310x310.png -------------------------------------------------------------------------------- /public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoschwarzer/gitlab-monitor/867f71cb88b5d63344c7c53c357e3a934f39e290/public/mstile-70x70.png -------------------------------------------------------------------------------- /scripts/wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -z "${GITLAB_MONITOR_CONFIG}" ]; then 4 | echo "${GITLAB_MONITOR_CONFIG}" > /usr/share/nginx/html/config.json 5 | fi 6 | exec nginx -g "daemon off;" 7 | -------------------------------------------------------------------------------- /src/Config.js: -------------------------------------------------------------------------------- 1 | import { getQueryParameter } from './util' 2 | import merge from 'deepmerge' 3 | import defaultConfig from './config.default' 4 | import base64Url from 'base64url' 5 | import YAML from 'yaml' 6 | 7 | export default new class Config { 8 | constructor() { 9 | this.config = null 10 | this.localConfig = null 11 | this.styleOverride = '' 12 | } 13 | 14 | load(config = null, style = null) { 15 | const rawConfig = getQueryParameter('rawConfig') 16 | 17 | if (style !== null) { 18 | this.styleOverride = style 19 | } 20 | 21 | if (config !== null) { 22 | this.localConfig = config 23 | this.config = merge(defaultConfig, config) 24 | } else if (rawConfig !== null) { 25 | this.localConfig = YAML.parse(base64Url.decode(rawConfig)) 26 | this.config = merge(defaultConfig, this.localConfig) 27 | } else { 28 | this.loadFromLocalStorage() 29 | } 30 | 31 | this.persist() 32 | } 33 | 34 | loadFromLocalStorage() { 35 | const config = window.localStorage.getItem('config') 36 | this.styleOverride = window.localStorage.getItem('styleOverride') || '' 37 | 38 | if (config !== null) { 39 | const localConfig = YAML.parse(config) 40 | this.config = merge(defaultConfig, localConfig) 41 | this.localConfig = localConfig 42 | } 43 | } 44 | 45 | persist() { 46 | if (!this.localConfig) { 47 | return 48 | } 49 | 50 | window.localStorage.setItem('config', YAML.stringify(this.localConfig, null, 2)) 51 | window.localStorage.setItem('styleOverride', this.styleOverride) 52 | } 53 | 54 | get isConfigured() { 55 | return this.config !== null 56 | } 57 | 58 | get root() { 59 | return this.config 60 | } 61 | 62 | get local() { 63 | return this.localConfig 64 | } 65 | 66 | get style() { 67 | return this.styleOverride 68 | } 69 | 70 | getProjectProperty(property, pathWithNamespace = '*') { 71 | if (this.root.projectConfig.hasOwnProperty(pathWithNamespace)) { 72 | return this.root.projectConfig[pathWithNamespace][property] 73 | } else { 74 | return this.root.projectConfig['*'][property] 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/GitLabApi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import parse from 'parse-link-header' 3 | import Vue from 'vue' 4 | import Config from './Config' 5 | 6 | const GitLabApi = {} 7 | 8 | GitLabApi.install = (Vue, options) => { 9 | Vue.prototype.$api = async (path, params = {}, behaviour = {}, method = 'GET') => { 10 | const response = await axios(path, { 11 | baseURL: Config.root.gitlabApi, 12 | method: method, 13 | params, 14 | headers: { 'Private-Token': Config.root.privateToken } 15 | }) 16 | const result = response.data 17 | if (behaviour.follow_next_page_links) { 18 | // Find the "next" link header and follow it, until we get a result that has no next link 19 | let parsedLinks = parse(response.headers.link) 20 | while (parsedLinks && parsedLinks.next) { 21 | const nextResponse = await axios.get(parsedLinks.next.url, { 22 | headers: { 'Private-Token': Config.root.privateToken } 23 | }) 24 | result.push(...nextResponse.data) 25 | parsedLinks = parse(nextResponse.headers.link) 26 | } 27 | } 28 | return result 29 | } 30 | } 31 | 32 | export default GitLabApi 33 | 34 | export function configureApi() { 35 | Vue.use(GitLabApi, { 36 | gitlab_api_url: Config.root.gitlabApi, 37 | private_token: Config.root.privateToken 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/backdrop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/app.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 328 | 329 | 374 | 375 | 464 | -------------------------------------------------------------------------------- /src/components/gitlab-icon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/components/job-view.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 78 | 79 | 170 | -------------------------------------------------------------------------------- /src/components/pipeline-view.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 214 | 215 | 342 | -------------------------------------------------------------------------------- /src/components/project-card.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 368 | 369 | 475 | -------------------------------------------------------------------------------- /src/components/runner-status.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 81 | 82 | 118 | -------------------------------------------------------------------------------- /src/components/stage-view.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 92 | -------------------------------------------------------------------------------- /src/components/test-report.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 36 | 37 | -------------------------------------------------------------------------------- /src/config.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitlabApi": "https://gitlab.example.com/api/v4", 3 | "privateToken": "ABCDEF123456", 4 | "maxAge": 168, 5 | "fetchCount": 20, 6 | "pipelinesOnly": false, 7 | "includeArchived": false, 8 | "autoZoom": false, 9 | "showPipelineIds": true, 10 | "showProjectOnlyOn": [], 11 | "showJobs": "icon", 12 | "showJobsNameOnlyOn": [], 13 | "showRestartedJobs": true, 14 | "showStagesNames": false, 15 | "showDurations": true, 16 | "showCoverage": false, 17 | "showTestReport": false, 18 | "showUsers": false, 19 | "showRerunButton": false, 20 | "title": null, 21 | "orderBy": "lastActivity", 22 | "orderByDesc": false, 23 | "pollingIntervalMultiplier": 1.0, 24 | "backgroundRefresh": true, 25 | "projectVisibility": "any", 26 | "membership": false, 27 | "badges": false, 28 | "showRunnerStatus": true, 29 | "filter": { 30 | "include": ".*", 31 | "includeTags": ".*", 32 | "exclude": null, 33 | "excludeTags": null, 34 | "excludeUntagged": false 35 | }, 36 | "projectConfig": { 37 | "*": { 38 | "include": ".*", 39 | "exclude": null, 40 | "default": null, 41 | "showMerged": true, 42 | "showTags": true, 43 | "showLatestTagOnly": false, 44 | "showDetached": false, 45 | "showLabels": true, 46 | "historyCount": 1, 47 | "hideSkippedPipelines": false, 48 | "hideSuccessfulPipelines": false, 49 | "soundAlerts": { 50 | "soundUrl": null, 51 | "include": ".*", 52 | "exclude": null 53 | } 54 | } 55 | }, 56 | "projectScope": null, 57 | "projectScopeId": null, 58 | "theme": null 59 | } 60 | -------------------------------------------------------------------------------- /src/config.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitlabApi": "https://gitlab.example.com/api/v4", 3 | "privateToken": "ABCDEF1234" 4 | } 5 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime' 2 | import Vue from 'vue' 3 | import VueTimeago from 'vue-timeago' 4 | import App from './components/app.vue' 5 | import Config from './Config' 6 | import {configureApi} from './GitLabApi' 7 | import axios from 'axios' 8 | 9 | const finish = () => { 10 | if (Config.isConfigured) { 11 | configureApi() 12 | } 13 | 14 | Vue.use(VueTimeago, { 15 | name: 'timeago', // component name, `timeago` by default 16 | locale: 'en', 17 | locales: { 18 | 'en': require('date-fns/locale/en') 19 | } 20 | }) 21 | 22 | new Vue({ 23 | el: '#app', 24 | render: h => h(App) 25 | }) 26 | } 27 | 28 | // Load bundled config, if present. 29 | ;(async () => { 30 | try { 31 | const {data} = await axios.get('./config.json') 32 | Config.load(data) 33 | } catch (e) { 34 | Config.load() 35 | } finally { 36 | finish() 37 | } 38 | })() 39 | -------------------------------------------------------------------------------- /src/themes/nord-dark.theme.scss: -------------------------------------------------------------------------------- 1 | @import "~nord/src/sass/nord.scss"; 2 | 3 | $night-darkest: $nord0; 4 | $night-dark: $nord1; 5 | $night-light: $nord2; 6 | $night-lightest: $nord3; 7 | 8 | $snow-dark: $nord4; 9 | $snow: $nord5; 10 | $snow-light: $nord6; 11 | 12 | $frost-green: $nord7; 13 | $frost-teal: $nord8; 14 | $frost-light-blue: $nord9; 15 | $frost-dark-blue: $nord10; 16 | 17 | $aurora-red: $nord11; 18 | $aurora-orange: $nord12; 19 | $aurora-yellow: $nord13; 20 | $aurora-green: $nord14; 21 | $aurora-purple: $nord15; 22 | 23 | @import '~@openfonts/roboto-condensed_all/index.css'; 24 | @import '~@openfonts/roboto_all/index.css'; 25 | 26 | .nord-dark { 27 | --background-color: #{$night-darkest}; 28 | --color: #{$snow-light}; 29 | --font-family: system-ui, sans-serif; 30 | --background: none; 31 | 32 | --project-default: #{$night-light}; 33 | --project-success: #{$aurora-green}; 34 | --project-running: #{$frost-dark-blue}; 35 | --project-pending: #{$aurora-orange}; 36 | --project-failed: #{$aurora-red}; 37 | --project-canceled: #{$night-dark}; 38 | --project-skipped: #{$night-lightest}; 39 | 40 | --project-spinner-color: #{$snow-dark}; 41 | --project-no-pipelines: #{$snow-dark}; 42 | --project-info-color: #{$snow-dark}; 43 | 44 | $pipeline-text-color: $snow-light; 45 | $pipeline-icons-color: $snow-dark; 46 | 47 | --pipeline-hashtag: #{$pipeline-text-color}; 48 | --pipeline-id: #{$pipeline-text-color}; 49 | --pipeline-branch: #{$pipeline-text-color}; 50 | --pipeline-clock-icon: #{$pipeline-icons-color}; 51 | --pipeline-duration: #{$pipeline-text-color}; 52 | --pipeline-user-icon: #{$pipeline-icons-color}; 53 | --pipeline-user: #{$pipeline-text-color}; 54 | 55 | --job-border-color: #{$snow-dark}; 56 | --job-connector-color: #{$snow-dark}; 57 | --job-icon-fill-color: #{$snow-dark}; 58 | --job-stage-names-color: #{$snow-dark}; 59 | --jop-icon-size: 18px; 60 | 61 | --job-success: #{$aurora-green}; 62 | --job-running: #{$frost-dark-blue}; 63 | --job-pending: #{$aurora-orange}; 64 | --job-warning: #{$aurora-yellow}; 65 | --job-failed: #{$aurora-red}; 66 | --job-canceled: #{$night-darkest}; 67 | --job-skipped: #{$night-light}; 68 | --job-manual: #{$night-light}; 69 | 70 | body { 71 | background: none; 72 | } 73 | 74 | .pipeline-container { 75 | margin-bottom: 8px; 76 | display: flex; 77 | flex-direction: column; 78 | 79 | .spinner-icon { 80 | align-self: center; 81 | justify-self: center; 82 | margin-bottom: 8px; 83 | } 84 | } 85 | 86 | .pipeline-view { 87 | &:not(:last-child) { 88 | margin-bottom: 8px; 89 | } 90 | 91 | .pipeline { 92 | height: 22px; 93 | font-size: 12px; 94 | 95 | .duration { 96 | font-size: 12px; 97 | } 98 | 99 | .pipeline-icon { 100 | width: 10px !important; 101 | margin-right: 4px; 102 | } 103 | } 104 | 105 | .branch { 106 | font-size: 12px; 107 | 108 | svg { 109 | height: 12px !important; 110 | margin-right: 6px !important; 111 | } 112 | } 113 | } 114 | 115 | .job-view .job-circle { 116 | font-size: 10px; 117 | 118 | &.created { 119 | color: $night-lightest; 120 | } 121 | } 122 | 123 | .project-card { 124 | color: var(--pipeline-status-color) !important; 125 | 126 | //&.success { 127 | // color: $night-darkest; 128 | // 129 | // --pipeline-hashtag: #{$night-darkest}; 130 | // --pipeline-id: #{$night-darkest}; 131 | // --pipeline-branch: #{$night-darkest}; 132 | // --pipeline-clock-icon: #{$night-dark}; 133 | // --pipeline-duration: #{$night-darkest}; 134 | // --pipeline-user-icon: #{$night-dark}; 135 | // --pipeline-user: #{$night-darkest}; 136 | //} 137 | 138 | .info { 139 | padding: 0.3rem; 140 | font-size: 0.5rem !important; 141 | } 142 | 143 | .content { 144 | padding-bottom: 0; 145 | 146 | .title { 147 | text-shadow: 0 0 0.5px rgba(0, 0, 0, 0.4); 148 | 149 | &:not(.small) { 150 | font-family: 'Roboto Condensed', sans-serif; 151 | } 152 | 153 | &.small { 154 | line-height: 1; 155 | font-weight: 400; 156 | font-size: 10px; 157 | } 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/themes/nord-light.theme.scss: -------------------------------------------------------------------------------- 1 | @import "~nord/src/sass/nord.scss"; 2 | 3 | $night-darkest: $nord0; 4 | $night-dark: $nord1; 5 | $night-light: $nord2; 6 | $night-lightest: $nord3; 7 | 8 | $snow-dark: $nord4; 9 | $snow: $nord5; 10 | $snow-light: $nord6; 11 | 12 | $frost-green: $nord7; 13 | $frost-teal: $nord8; 14 | $frost-light-blue: $nord9; 15 | $frost-dark-blue: $nord10; 16 | 17 | $aurora-red: $nord11; 18 | $aurora-orange: $nord12; 19 | $aurora-yellow: $nord13; 20 | $aurora-green: $nord14; 21 | $aurora-purple: $nord15; 22 | 23 | @import '~@openfonts/roboto-condensed_all/index.css'; 24 | @import '~@openfonts/roboto_all/index.css'; 25 | 26 | .nord-light { 27 | --background-color: #{$snow-light}; 28 | --color: #{$night-light}; 29 | --font-family: 'Roboto', sans-serif; 30 | --background: none; 31 | --loading-overlay-color: transparent; 32 | --loading-indicator-color: #{$snow-dark}; 33 | 34 | --project-default: #fff; 35 | --project-success: #{$aurora-green}; 36 | --project-running: #{$frost-dark-blue}; 37 | --project-pending: #{$aurora-orange}; 38 | --project-failed: #{$aurora-red}; 39 | --project-canceled: #{$night-dark}; 40 | --project-skipped: #{$night-lightest}; 41 | 42 | --project-spinner-color: #{$snow-dark}; 43 | --project-no-pipelines: #{$snow-dark}; 44 | --project-info-color: #{$snow-dark}; 45 | 46 | $pipeline-text-color: $night-lightest; 47 | $pipeline-icons-color: $night-dark; 48 | 49 | --pipeline-hashtag: #{$pipeline-text-color}; 50 | --pipeline-id: #{$pipeline-text-color}; 51 | --pipeline-branch: #{$pipeline-icons-color}; 52 | --pipeline-clock-icon: #{$pipeline-icons-color}; 53 | --pipeline-duration: #{$pipeline-text-color}; 54 | --pipeline-user-icon: #{$pipeline-icons-color}; 55 | --pipeline-user: #{$pipeline-text-color}; 56 | 57 | --job-border-color: #{$snow-dark}; 58 | --job-connector-color: #{$snow-dark}; 59 | --job-stage-names-color: #{$night-lightest}; 60 | --jop-icon-size: 18px; 61 | 62 | --job-created: #{$snow-dark}; 63 | --job-success: #{$aurora-green}; 64 | --job-running: #{$frost-dark-blue}; 65 | --job-pending: #{$aurora-orange}; 66 | --job-warning: #{$aurora-yellow}; 67 | --job-failed: #{$aurora-red}; 68 | --job-canceled: #{$night-darkest}; 69 | --job-skipped: #{$night-light}; 70 | --job-manual: #{$night-light}; 71 | 72 | body { 73 | background: none; 74 | } 75 | 76 | .job-view .job-circle { 77 | --job-icon-fill-color: var(--job-status-color); 78 | 79 | background-color: $snow-light; 80 | border-color: var(--job-status-color, $snow-dark); 81 | font-size: 10px; 82 | color: var(--job-status-color); 83 | 84 | &.created { 85 | color: $night-lightest; 86 | } 87 | } 88 | 89 | .stage-view .stage-title { 90 | text-align: center !important; 91 | } 92 | 93 | .pipeline-view { 94 | margin-bottom: 8px; 95 | 96 | .pipeline { 97 | height: 22px; 98 | font-size: 12px; 99 | 100 | .duration { 101 | font-size: 12px; 102 | } 103 | 104 | .pipeline-icon { 105 | width: 10px !important; 106 | margin-right: 4px; 107 | } 108 | 109 | .skipped { 110 | line-height: var(--job-icon-size); 111 | color: $night-lightest; 112 | padding-top: 0; 113 | padding-bottom: 0; 114 | border-radius: 999px; 115 | 116 | .pipeline-icon { 117 | width: 18px !important; 118 | height: 18px !important; 119 | } 120 | } 121 | } 122 | 123 | .branch { 124 | font-size: 12px; 125 | 126 | svg { 127 | height: 12px !important; 128 | margin-right: 6px !important; 129 | } 130 | } 131 | } 132 | 133 | .project-card { 134 | background-color: white !important; 135 | box-shadow: 0 3px + 100px 0 -100px inset var(--project-card-status-color); 136 | 137 | .info { 138 | padding: 0.5rem; 139 | font-size: 0.6rem !important; 140 | } 141 | 142 | .content { 143 | padding-bottom: 0; 144 | 145 | .title { 146 | text-shadow: none; 147 | 148 | &:not(.small) { 149 | font-family: 'Roboto Condensed', sans-serif; 150 | } 151 | 152 | &.small { 153 | line-height: 1; 154 | font-size: 10px; 155 | color: $night-lightest; 156 | } 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string' 2 | 3 | export const getQueryParameter = (name) => { 4 | const parsed = queryString.parse(location.search) 5 | 6 | if (name in parsed) { 7 | let parameter = parsed[name] 8 | 9 | if (parameter === 'true') { 10 | return true 11 | } else if (parameter === 'false') { 12 | return false 13 | } else if (!isNaN(+parameter)) { 14 | return +parameter 15 | } 16 | 17 | return parameter 18 | } else { 19 | return null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const MonacoEditorPlugin = require('monaco-editor-webpack-plugin') 2 | 3 | module.exports = { 4 | publicPath: '.', 5 | configureWebpack: { 6 | plugins: [ 7 | new MonacoEditorPlugin({ 8 | // https://github.com/Microsoft/monaco-editor-webpack-plugin#options 9 | // Include a subset of languages support 10 | // Some language extensions like typescript are so huge that may impact build performance 11 | // e.g. Build full languages support with webpack 4.0 takes over 80 seconds 12 | // Languages are loaded on demand at runtime 13 | languages: ['css', 'yaml'] 14 | }) 15 | ] 16 | } 17 | }; 18 | --------------------------------------------------------------------------------