Merge branch 'master' of github.com:spotify/backstage into mob/create-vcs-step
* 'master' of github.com:spotify/backstage: (62 commits) chore: ref to shadowDomRef chore: revert data-testid back to shadow-dom fix: remove re-creating new shadow-dom on page change fix(techdocs): cleaned up useEffect in Reader fix: moved useEnforcingTrailingSlash upper in the hook chain chore(techdocs): updated tests fix(techdocs): fixed race condition between page loads fix(useFetch): use AsyncState type fix(techdocs-404): throw error fix(techdocs-home): move docs sites mock data to array fix(test): change to lowercase fix(test): remove async refactor(techdocs): basic techdocs not found, split out components from reader fix(tests): fix fixures (#1505) build(deps): bump css-loader from 3.5.3 to 3.6.0 cli: never treat success as a failure in e2e test Fix variable Use Config for Filter Org List Header fix(sidebar): do not let sidebar items collapse and shrink cli: use tree-kill to kill serve task in e2e test ...
@@ -43,23 +43,4 @@ jobs:
|
||||
- run: yarn tsc
|
||||
- run: yarn build
|
||||
- name: verify app and plugin creation
|
||||
working-directory: ${{ runner.temp }}
|
||||
run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: lint newly created app and plugin
|
||||
run: yarn lint:all
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: test newly created app and plugin
|
||||
run: yarn test:all
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: e2e test newly created app
|
||||
run: yarn test:e2e:ci
|
||||
working-directory: ${{ runner.temp }}/test-app/packages/app
|
||||
env:
|
||||
APP_CONFIG_app_baseUrl: '"http://localhost:3001"'
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
|
||||
@@ -44,25 +44,6 @@ jobs:
|
||||
- run: yarn tsc
|
||||
- run: yarn build
|
||||
- name: verify app and plugin creation
|
||||
working-directory: ${{ runner.temp }}
|
||||
run: |
|
||||
sudo sysctl fs.inotify.max_user_watches=524288
|
||||
node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: lint newly created app and plugin
|
||||
run: yarn lint:all
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: test newly created app and plugin
|
||||
run: yarn test:all
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: e2e test newly created app
|
||||
run: yarn test:e2e:ci
|
||||
working-directory: ${{ runner.temp }}/test-app/packages/app
|
||||
env:
|
||||
APP_CONFIG_app_baseUrl: '"http://localhost:3001"'
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# ADR006: Avoid React.FC and React.SFC
|
||||
|
||||
## Context
|
||||
|
||||
Facebook has removed `React.FC` from their base template for a Typescript project. The reason for this was that it was found to be an unnecessary feature with next to no benefits in combination with a few downsides.
|
||||
|
||||
The main reasons were:
|
||||
- **children props** were implicitly added
|
||||
- **Generic Type** were not supported on children
|
||||
|
||||
Read more about the removal in [this PR](https://github.com/facebook/create-react-app/pull/8177).
|
||||
|
||||
## Decision
|
||||
|
||||
To keep our codebase up to date, we have decided that `React.FC` and `React.SFC` should be avoided in our codebase when adding new code.
|
||||
|
||||
Here is an example:
|
||||
```ts
|
||||
/* Avoid this: */
|
||||
type BadProps = { text: string; };
|
||||
const BadComponent: FC<BadProps> = ({ text, children }) => (
|
||||
<div>
|
||||
<div>{text}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
/* Do this instead: */
|
||||
type GoodProps = { text: string; children?: React.ReactNode; };
|
||||
const GoodComponent = ({ text, children }: GoodProps) => (
|
||||
<div>
|
||||
<div>{text}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
We will gradually remove the current usage of `React.FC` and `React.SFC` from our codebase.
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backstage
|
||||
labels:
|
||||
app: backstage
|
||||
component: frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backstage
|
||||
component: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backstage
|
||||
component: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: spotify/backstage:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: app
|
||||
protocol: TCP
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backstage-backend
|
||||
labels:
|
||||
app: backstage
|
||||
component: backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backstage
|
||||
component: backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backstage
|
||||
component: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: spotify/backstage-backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 7000
|
||||
name: backend
|
||||
protocol: TCP
|
||||
@@ -0,0 +1,22 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
appVersion: "1.0"
|
||||
description: A Helm chart for Spotify Backstage
|
||||
name: backstage
|
||||
version: 0.1.1-alpha.12
|
||||
@@ -0,0 +1,51 @@
|
||||
# Backstage Helm Chart
|
||||
|
||||
## App/Frontend Values
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| --------------------------- | --------------------------------------------------------- | ------------------- |
|
||||
| app.enabled | Whether to render the frontend app config or not | `true` |
|
||||
| app.nameOverride | Override the name given to the app/frontend | `""` |
|
||||
| app.fullnameOverride | Override the full name of the app/frontend | `""` |
|
||||
| app.replicaCount | The number of replicas for the app/frontend | `1` |
|
||||
| app.image.repository | The image repository of the app/frontend container | `spotify/backstage` |
|
||||
| app.image.tag | The app/frontend tag to pull | `latest` |
|
||||
| app.image.pullPolicy | Image pull policy | `Always` |
|
||||
| app.service.type | The service type for the app/frontend service | `ClusterIP` |
|
||||
| app.service.port | The port for the app/frontend | `80` |
|
||||
| app.ingress.enabled | Whether to create ingress or not | `false` |
|
||||
| app.ingress.annotations | Annotations for the app/frontend ingress | `{}` |
|
||||
| app.ingress.hosts[].host | Hostname for the app/frontend | `backstage.local` |
|
||||
| app.ingress.hosts[].paths[] | Path name to serve the app/frontend on | `["/"]` |
|
||||
| app.imagePullSecrets[] | Any image secrets you need to pull `app.image.repository` | `[]` |
|
||||
| app.podSecurityContext | Security context for the app/frontend pods | `{}` |
|
||||
| app.securityContext | Security context settings for the deployment | `{}` |
|
||||
| app.resources | Kubernetes Pod resource requests/limits | `{}` |
|
||||
| app.nodeSelector | Node selectors for scheduling app/frontend pods | `{}` |
|
||||
| app.tolerations | Tolerations for scheduling app/frontend pods | `{}` |
|
||||
| app.affinity | Affinity setttings for scheduling app/frontend pods | `{}` |
|
||||
|
||||
## Backend Values
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| ------------------------------- | ------------------------------------------------------------- | ------------------- |
|
||||
| backend.enabled | Whether to render the backend config or not | `true` |
|
||||
| backend.nameOverride | Override the name given to the backend | `""` |
|
||||
| backend.fullnameOverride | Override the full name of the backend | `""` |
|
||||
| backend.replicaCount | The number of replicas for the backend | `1` |
|
||||
| backend.image.repository | The image repository of the backend container | `spotify/backstage` |
|
||||
| backend.image.tag | The backend tag to pull | `latest` |
|
||||
| backend.image.pullPolicy | Image pull policy | `Always` |
|
||||
| backend.service.type | The service type for the backend service | `ClusterIP` |
|
||||
| backend.service.port | The port for the backend | `80` |
|
||||
| backend.ingress.enabled | Whether to create ingress or not | `false` |
|
||||
| backend.ingress.annotations | Annotations for the backend ingress | `{}` |
|
||||
| backend.ingress.hosts[].host | Hostname for the backend | `backstage.local` |
|
||||
| backend.ingress.hosts[].paths[] | Path name to serve the backend on | `["/"]` |
|
||||
| backend.imagePullSecrets[] | Any image secrets you need to pull `backend.image.repository` | `[]` |
|
||||
| backend.podSecurityContext | Security context for the backend pods | `{}` |
|
||||
| backend.securityContext | Security context settings for the deployment | `{}` |
|
||||
| backend.resources | Kubernetes Pod resource requests/limits | `{}` |
|
||||
| backend.nodeSelector | Node selectors for scheduling backend pods | `{}` |
|
||||
| backend.tolerations | Tolerations for scheduling backend pods | `{}` |
|
||||
| backend.affinity | Affinity setttings for scheduling backend pods | `{}` |
|
||||
@@ -0,0 +1,80 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "backstage.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "backstage.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "backstage.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common App labels
|
||||
*/}}
|
||||
{{- define "backstage.app.labels" -}}
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}-app
|
||||
helm.sh/chart: {{ include "backstage.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common Backend labels
|
||||
*/}}
|
||||
{{- define "backstage.backend.labels" -}}
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}-backend
|
||||
helm.sh/chart: {{ include "backstage.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use for the app
|
||||
*/}}
|
||||
{{- define "backstage.app.serviceAccountName" -}}
|
||||
{{- if .Values.app.serviceAccount.create -}}
|
||||
{{ default "default" .Values.app.serviceAccount.name }}
|
||||
{{- else -}}
|
||||
{{ default "default" .Values.app.serviceAccount.name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use for the backend
|
||||
*/}}
|
||||
{{- define "backstage.backend.serviceAccountName" -}}
|
||||
{{- if .Values.backend.serviceAccount.create -}}
|
||||
{{ default default .Values.backend.serviceAccount.name }}
|
||||
{{- else -}}
|
||||
{{ default "default" .Values.backend.serviceAccount.name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,119 @@
|
||||
{{- if .Values.app.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "backstage.fullname" . }}-app
|
||||
labels:
|
||||
{{ include "backstage.app.labels" . | indent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.app.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}-app
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}-app
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
{{- with .Values.app.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ template "backstage.app.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.app.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}-app
|
||||
securityContext:
|
||||
{{- toYaml .Values.app.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
|
||||
ports:
|
||||
- name: app
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.app.resources | nindent 12 }}
|
||||
{{- with .Values.app.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.app.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.app.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.enabled }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "backstage.fullname" . }}-backend
|
||||
labels:
|
||||
{{ include "backstage.backend.labels" . | indent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.backend.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}-backend
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}-backend
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
{{- with .Values.backend.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ template "backstage.backend.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.backend.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}-backend
|
||||
securityContext:
|
||||
{{- toYaml .Values.backend.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
|
||||
ports:
|
||||
- name: backend
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: backend
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: backend
|
||||
resources:
|
||||
{{- toYaml .Values.backend.resources | nindent 12 }}
|
||||
{{- with .Values.backend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,83 @@
|
||||
{{- if .Values.app.ingress.enabled -}}
|
||||
{{- $fullName := include "backstage.fullname" . -}}
|
||||
{{- $svcPort := .Values.app.service.port -}}
|
||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-app
|
||||
labels:
|
||||
{{ include "backstage.app.labels" . | indent 4 }}
|
||||
{{- with .Values.app.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.app.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.app.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.app.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ . }}
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-app
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
{{- if .Values.backend.ingress.enabled }}
|
||||
---
|
||||
{{- $fullName := include "backstage.fullname" . -}}
|
||||
{{- $svcPort := .Values.backend.service.port -}}
|
||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion }}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else }}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-backend
|
||||
labels:
|
||||
{{ include "backstage.backend.labels" . | indent 4 }}
|
||||
{{- with .Values.backend.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.backend.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.backend.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.backend.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ . }}
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-backend
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{- if .Values.app.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "backstage.fullname" . }}-app
|
||||
labels:
|
||||
{{ include "backstage.app.labels" . | indent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.app.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.app.service.port }}
|
||||
targetPort: app
|
||||
protocol: TCP
|
||||
name: app
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
{{- if .Values.app.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "backstage.fullname" . }}-backend
|
||||
labels:
|
||||
{{ include "backstage.backend.labels" . | indent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.backend.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.backend.service.port }}
|
||||
targetPort: backend
|
||||
protocol: TCP
|
||||
name: backend
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "backstage.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,95 @@
|
||||
app:
|
||||
enabled: true
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
replicaCount: 1
|
||||
serviceAccount:
|
||||
create: false
|
||||
Name: ""
|
||||
image:
|
||||
repository: spotify/backstage
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
hosts:
|
||||
- host: backstage.local
|
||||
paths:
|
||||
- /
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
imagePullSecrets: []
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
resources: {}
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
backend:
|
||||
enabled: false
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
replicaCount: 1
|
||||
serviceAccount:
|
||||
create: false
|
||||
Name: ""
|
||||
image:
|
||||
repository: spotify/backstage-backend
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 7000
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
hosts:
|
||||
- host: backstage.local
|
||||
paths:
|
||||
- /backend
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
imagePullSecrets: []
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
resources: {}
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: backstage
|
||||
labels:
|
||||
app: backstage
|
||||
component: ingress
|
||||
spec:
|
||||
rules:
|
||||
- host: <HOSTNAME>
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: backstage
|
||||
servicePort: frontend
|
||||
path: /
|
||||
- backend:
|
||||
serviceName: backstage-backend
|
||||
servicePort: backend
|
||||
path: /backend
|
||||
@@ -0,0 +1,35 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: backstage
|
||||
labels:
|
||||
app: backstage
|
||||
component: frontend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: backstage
|
||||
component: frontend
|
||||
ports:
|
||||
- name: frontend
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: app
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: backstage-backend
|
||||
labels:
|
||||
app: backstage
|
||||
component: backend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: backstage
|
||||
component: backend
|
||||
ports:
|
||||
- name: backend
|
||||
port: 7000
|
||||
protocol: TCP
|
||||
targetPort: backend
|
||||
@@ -8,6 +8,7 @@
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-circleci": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-explore": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-github-actions": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-graphiql": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-lighthouse": "^0.1.1-alpha.12",
|
||||
@@ -21,12 +22,13 @@
|
||||
"@backstage/theme": "^0.1.1-alpha.12",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"history": "^5.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-router": "6.0.0-alpha.5",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 890 B After Width: | Height: | Size: 883 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg id="favicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 337.46 428.5">
|
||||
<title>
|
||||
Backstage favicon
|
||||
</title>
|
||||
<style>
|
||||
#favicon path {
|
||||
fill: #121212;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#favicon path {
|
||||
fill: #7df3e1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path d="M303,166.05a80.69,80.69,0,0,0,13.45-10.37c.79-.77,1.55-1.53,2.3-2.3a83.12,83.12,0,0,0,7.93-9.38A63.69,63.69,0,0,0,333,133.23a48.58,48.58,0,0,0,4.35-16.4c1.49-19.39-10-38.67-35.62-54.22L198.56,0,78.3,115.23,0,190.25l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.69,19.16-18.36,25.52-42.12,13.7-61.87a49.22,49.22,0,0,0-6.8-8.87A89.17,89.17,0,0,0,259,178.29h.15a85.08,85.08,0,0,0,31-5.79A80.88,80.88,0,0,0,303,166.05ZM202.45,225.86c-19.32,18.51-50.4,21.23-75.7,5.9L51.61,186.15l67.45-64.64,76.41,46.38C223,184.58,221.49,207.61,202.45,225.86Zm8.93-82.22-70.65-42.89L205.14,39,274.51,81.1c25.94,15.72,29.31,37,10.55,55A60.69,60.69,0,0,1,211.38,143.64Zm29.86,190c-19.57,18.75-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,282.52v24.67L108.6,373.1a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A87.27,87.27,0,0,1,241.24,333.68Zm0-39c-19.57,18.75-46.17,29.08-74.88,29.08a123.81,123.81,0,0,1-64.1-18.19L0,243.53v24.68l108.6,65.91a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.5v-1.78A87.27,87.27,0,0,1,241.24,294.7Zm0-39c-19.57,18.76-46.17,29.09-74.88,29.09a123.81,123.81,0,0,1-64.1-18.19L0,204.55v24.68l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.68,12.88-12.35,20-27.13,19.68-41.5v-1.82A86.09,86.09,0,0,1,241.24,255.71Zm83.7,25.74a94.15,94.15,0,0,1-60.2,25.86h0V334a81.6,81.6,0,0,0,51.74-22.37c14-13.38,21.14-28.11,21-42.64v-2.19A94.92,94.92,0,0,1,324.94,281.45Zm-83.7,91.21c-19.57,18.76-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,321.5v24.68l108.6,65.9a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.8,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A86.29,86.29,0,0,1,241.24,372.66ZM327,162.45c-.68.69-1.35,1.38-2.05,2.06a94.37,94.37,0,0,1-10.64,8.65,91.35,91.35,0,0,1-11.6,7,94.53,94.53,0,0,1-26.24,8.71,97.69,97.69,0,0,1-14.16,1.57c.5,1.61.9,3.25,1.25,4.9a53.27,53.27,0,0,1,1.14,12V217h.05a84.41,84.41,0,0,0,25.35-5.55,81,81,0,0,0,26.39-16.82c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,172.17a48.55,48.55,0,0,0,4.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08,1.54-2.25,3.09A96.73,96.73,0,0,1,327,162.45Zm0,77.92c-.69.7-1.31,1.41-2,2.1a94.2,94.2,0,0,1-60.2,25.86h0l0,26.67h0a81.6,81.6,0,0,0,51.74-22.37A73.51,73.51,0,0,0,333,250.13a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.47.19-3.71v-2.19c-.74,1.07-1.46,2.15-2.27,3.21A95.68,95.68,0,0,1,327,240.37Zm0-39c-.69.7-1.31,1.41-2,2.1a93.18,93.18,0,0,1-10.63,8.65,91.63,91.63,0,0,1-11.63,7,95.47,95.47,0,0,1-37.94,10.18h0V256h0a81.65,81.65,0,0,0,51.74-22.37c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,211.15a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.48.19-3.71v-2.2c-.74,1.08-1.46,2.16-2.27,3.22A95.68,95.68,0,0,1,327,201.39Z"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -26,12 +26,14 @@ import {
|
||||
FeatureFlags,
|
||||
GoogleAuth,
|
||||
GithubAuth,
|
||||
OAuth2,
|
||||
OktaAuth,
|
||||
GitlabAuth,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
googleAuthApiRef,
|
||||
githubAuthApiRef,
|
||||
oauth2ApiRef,
|
||||
oktaAuthApiRef,
|
||||
gitlabAuthApiRef,
|
||||
storageApiRef,
|
||||
@@ -109,7 +111,16 @@ export const apis = (config: ConfigApi) => {
|
||||
builder.add(
|
||||
gitlabAuthApiRef,
|
||||
GitlabAuth.create({
|
||||
apiOrigin: 'http://localhost:7000',
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/auth/',
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
|
||||
builder.add(
|
||||
oauth2ApiRef,
|
||||
OAuth2.create({
|
||||
apiOrigin: backendUrl,
|
||||
basePath: '/auth/',
|
||||
oauthRequestApi,
|
||||
}),
|
||||
|
||||
@@ -25,3 +25,4 @@ export { plugin as Sentry } from '@backstage/plugin-sentry';
|
||||
export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles';
|
||||
export { plugin as TechDocs } from '@backstage/plugin-techdocs';
|
||||
export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
|
||||
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
export {};
|
||||
|
||||
@@ -25,7 +25,13 @@ You should only need to do this once.
|
||||
After that, go to the `packages/backend` directory and run
|
||||
|
||||
```bash
|
||||
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x SENTRY_TOKEN=x LOG_LEVEL=debug yarn start
|
||||
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \
|
||||
AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \
|
||||
AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \
|
||||
AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \
|
||||
SENTRY_TOKEN=x \
|
||||
LOG_LEVEL=debug \
|
||||
yarn start
|
||||
```
|
||||
|
||||
Substitute `x` for actual values, or leave them as
|
||||
|
||||
@@ -36,6 +36,7 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': 0, // Permitted in console programs
|
||||
'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()'
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-duplicates': 'warn',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
|
||||
@@ -41,6 +41,7 @@ module.exports = {
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js', '**/dist/**'],
|
||||
rules: {
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-duplicates': 'warn',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
|
||||
@@ -16,65 +16,238 @@
|
||||
|
||||
const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const { resolve: resolvePath } = require('path');
|
||||
const killTree = require('tree-kill');
|
||||
const { resolve: resolvePath, join: joinPath } = require('path');
|
||||
const Browser = require('zombie');
|
||||
|
||||
const {
|
||||
spawnPiped,
|
||||
runPlain,
|
||||
handleError,
|
||||
waitForPageWithText,
|
||||
waitFor,
|
||||
waitForExit,
|
||||
print,
|
||||
} = require('./helpers');
|
||||
|
||||
const createTestApp = require('./createTestApp');
|
||||
const createTestPlugin = require('./createTestPlugin');
|
||||
|
||||
Browser.localhost('localhost', 3000);
|
||||
|
||||
async function createTempDir() {
|
||||
return fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.env.BACKSTAGE_E2E_CLI_TEST = 'true';
|
||||
const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
|
||||
print(`CLI E2E test root: ${rootDir}\n`);
|
||||
|
||||
const workDir = process.env.CI ? process.cwd() : await createTempDir();
|
||||
print('Building dist workspace');
|
||||
const workspaceDir = await buildDistWorkspace('workspace', rootDir);
|
||||
|
||||
process.stdout.write(`Initial directory: ${process.cwd()}\n`);
|
||||
process.chdir(workDir);
|
||||
process.stdout.write(`Working directory: ${process.cwd()}\n`);
|
||||
print('Creating a Backstage App');
|
||||
const appDir = await createApp('test-app', workspaceDir, rootDir);
|
||||
|
||||
await createTestApp();
|
||||
|
||||
const appDir = resolvePath(workDir, 'test-app');
|
||||
process.chdir(appDir);
|
||||
process.stdout.write(`App directory: ${appDir}\n`);
|
||||
|
||||
await createTestPlugin();
|
||||
print('Creating a Backstage Plugin');
|
||||
const pluginName = await createPlugin('test-plugin', appDir);
|
||||
|
||||
print('Starting the app');
|
||||
const startApp = spawnPiped(['yarn', 'start']);
|
||||
await testAppServe(pluginName, appDir);
|
||||
|
||||
print('All tests successful, removing test dir');
|
||||
await fs.remove(rootDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a dist workspace that contains the cli and core packages
|
||||
*/
|
||||
async function buildDistWorkspace(workspaceName, rootDir) {
|
||||
const workspaceDir = resolvePath(rootDir, workspaceName);
|
||||
await fs.ensureDir(workspaceDir);
|
||||
|
||||
print(`Preparing workspace`);
|
||||
await runPlain([
|
||||
'yarn',
|
||||
'backstage-cli',
|
||||
'build-workspace',
|
||||
workspaceDir,
|
||||
'@backstage/cli',
|
||||
'@backstage/core',
|
||||
'@backstage/dev-utils',
|
||||
'@backstage/test-utils',
|
||||
]);
|
||||
|
||||
print('Pinning yarn version in workspace');
|
||||
await pinYarnVersion(workspaceDir);
|
||||
|
||||
print('Installing workspace dependencies');
|
||||
await runPlain(['yarn', 'install', '--production', '--frozen-lockfile'], {
|
||||
cwd: workspaceDir,
|
||||
});
|
||||
|
||||
return workspaceDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin the yarn version in a directory to the one we're using in the Backstage repo
|
||||
*/
|
||||
async function pinYarnVersion(dir) {
|
||||
const repoRoot = resolvePath(__dirname, '../../..');
|
||||
|
||||
const yarnRc = await fs.readFile(resolvePath(repoRoot, '.yarnrc'), 'utf8');
|
||||
const yarnRcLines = yarnRc.split('\n');
|
||||
const yarnPathLine = yarnRcLines.find(line => line.startsWith('yarn-path'));
|
||||
const [, localYarnPath] = yarnPathLine.match(/"(.*)"/);
|
||||
const yarnPath = resolvePath(repoRoot, localYarnPath);
|
||||
|
||||
await fs.writeFile(resolvePath(dir, '.yarnrc'), `yarn-path "${yarnPath}"\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new app inside rootDir called test-app, using packages from the workspaceDir
|
||||
*/
|
||||
async function createApp(appName, workspaceDir, rootDir) {
|
||||
const child = spawnPiped(
|
||||
[
|
||||
'node',
|
||||
resolvePath(workspaceDir, 'packages/cli/bin/backstage-cli'),
|
||||
'create-app',
|
||||
'--skip-install',
|
||||
],
|
||||
{
|
||||
cwd: rootDir,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
let stdout = '';
|
||||
child.stdout.on('data', data => {
|
||||
stdout = stdout + data.toString('utf8');
|
||||
});
|
||||
|
||||
await waitFor(() => stdout.includes('Enter a name for the app'));
|
||||
child.stdin.write(`${appName}\n`);
|
||||
|
||||
print('Waiting for app create script to be done');
|
||||
await waitForExit(child);
|
||||
|
||||
const appDir = resolvePath(rootDir, appName);
|
||||
|
||||
print('Rewriting module resolutions of app to use workspace packages');
|
||||
await overrideModuleResolutions(appDir, workspaceDir);
|
||||
|
||||
print('Pinning yarn version and registry in app');
|
||||
await pinYarnVersion(appDir);
|
||||
await fs.writeFile(
|
||||
resolvePath(appDir, '.npmrc'),
|
||||
'registry=https://registry.npmjs.org/\n',
|
||||
);
|
||||
|
||||
print('Test app created');
|
||||
|
||||
for (const cmd of ['install', 'tsc', 'build', 'lint:all', 'test:all']) {
|
||||
print(`Running 'yarn ${cmd}' in newly created app`);
|
||||
await runPlain(['yarn', cmd], { cwd: appDir });
|
||||
}
|
||||
|
||||
print(`Running 'yarn test:e2e:ci' in newly created app`);
|
||||
await runPlain(['yarn', 'test:e2e:ci'], {
|
||||
cwd: resolvePath(appDir, 'packages', 'app'),
|
||||
env: {
|
||||
...process.env,
|
||||
APP_CONFIG_app_baseUrl: '"http://localhost:3001"',
|
||||
},
|
||||
});
|
||||
|
||||
return appDir;
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This points dependency resolutions into the workspace for each package that is present there
|
||||
*/
|
||||
async function overrideModuleResolutions(appDir, workspaceDir) {
|
||||
const pkgJsonPath = resolvePath(appDir, 'package.json');
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
|
||||
pkgJson.resolutions = pkgJson.resolutions || {};
|
||||
pkgJson.dependencies = pkgJson.dependencies || {};
|
||||
|
||||
const packageNames = await fs.readdir(resolvePath(workspaceDir, 'packages'));
|
||||
for (const name of packageNames) {
|
||||
const pkgPath = joinPath('..', 'workspace', 'packages', name);
|
||||
|
||||
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
delete pkgJson.devDependencies[`@backstage/${name}`];
|
||||
}
|
||||
fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses create-plugin command to create a new plugin in the app
|
||||
*/
|
||||
async function createPlugin(pluginName, appDir) {
|
||||
const child = spawnPiped(['yarn', 'create-plugin'], {
|
||||
cwd: appDir,
|
||||
});
|
||||
|
||||
try {
|
||||
let stdout = '';
|
||||
child.stdout.on('data', data => {
|
||||
stdout = stdout + data.toString('utf8');
|
||||
});
|
||||
|
||||
await waitFor(() => stdout.includes('Enter an ID for the plugin'));
|
||||
child.stdin.write(`${pluginName}\n`);
|
||||
|
||||
// await waitFor(() => stdout.includes('Enter the owner(s) of the plugin'));
|
||||
// child.stdin.write('@someuser\n');
|
||||
|
||||
print('Waiting for plugin create script to be done');
|
||||
await waitForExit(child);
|
||||
|
||||
const pluginDir = resolvePath(appDir, 'plugins', pluginName);
|
||||
for (const cmd of [['lint'], ['test', '--no-watch']]) {
|
||||
print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`);
|
||||
await runPlain(['yarn', ...cmd], { cwd: pluginDir });
|
||||
}
|
||||
|
||||
return pluginName;
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start serving the newly created app and make sure that the create plugin is rendering correctly
|
||||
*/
|
||||
async function testAppServe(pluginName, appDir) {
|
||||
const startApp = spawnPiped(['yarn', 'start'], {
|
||||
cwd: appDir,
|
||||
});
|
||||
Browser.localhost('localhost', 3000);
|
||||
|
||||
let successful = false;
|
||||
try {
|
||||
const browser = new Browser();
|
||||
|
||||
await waitForPageWithText(browser, '/', 'Welcome to Backstage');
|
||||
await waitForPageWithText(
|
||||
browser,
|
||||
'/test-plugin',
|
||||
'Welcome to test-plugin!',
|
||||
`/${pluginName}`,
|
||||
`Welcome to ${pluginName}!`,
|
||||
);
|
||||
|
||||
print('Both App and Plugin loaded correctly');
|
||||
successful = true;
|
||||
} catch (error) {
|
||||
throw new Error(`App serve test failed, ${error}`);
|
||||
} finally {
|
||||
startApp.kill();
|
||||
// Kill entire process group, otherwise we'll end up with hanging serve processes
|
||||
killTree(startApp.pid);
|
||||
}
|
||||
|
||||
await waitForExit(startApp);
|
||||
|
||||
print('All tests done');
|
||||
process.exit(0);
|
||||
try {
|
||||
await waitForExit(startApp);
|
||||
} catch (error) {
|
||||
if (!successful) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', handleError);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { resolve: resolvePath } = require('path');
|
||||
const { spawnPiped, waitFor, waitForExit, print } = require('./helpers');
|
||||
|
||||
async function createTestApp() {
|
||||
const cliPath = resolvePath(__dirname, '../bin/backstage-cli');
|
||||
|
||||
print('Creating a Backstage App');
|
||||
const createApp = spawnPiped(['node', cliPath, 'create-app']);
|
||||
|
||||
try {
|
||||
let stdout = '';
|
||||
createApp.stdout.on('data', data => {
|
||||
stdout = stdout + data.toString('utf8');
|
||||
});
|
||||
|
||||
await waitFor(() => stdout.includes('Enter a name for the app'));
|
||||
createApp.stdin.write('test-app\n');
|
||||
|
||||
print('Waiting for app create script to be done');
|
||||
await waitForExit(createApp);
|
||||
|
||||
print('Test app created');
|
||||
} finally {
|
||||
createApp.kill();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createTestApp;
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { spawnPiped, waitFor, waitForExit, print } = require('./helpers');
|
||||
|
||||
async function createTestPlugin() {
|
||||
print('Creating a Backstage Plugin');
|
||||
const createPlugin = spawnPiped(['yarn', 'create-plugin']);
|
||||
|
||||
try {
|
||||
let stdout = '';
|
||||
createPlugin.stdout.on('data', data => {
|
||||
stdout = stdout + data.toString('utf8');
|
||||
});
|
||||
|
||||
await waitFor(() => stdout.includes('Enter an ID for the plugin'));
|
||||
createPlugin.stdin.write('test-plugin\n');
|
||||
|
||||
// await waitFor(() => stdout.includes('Enter the owner(s) of the plugin'));
|
||||
// createPlugin.stdin.write('@someuser\n');
|
||||
|
||||
print('Waiting for plugin create script to be done');
|
||||
await waitForExit(createPlugin);
|
||||
|
||||
print('Test plugin created');
|
||||
} finally {
|
||||
createPlugin.kill();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createTestPlugin;
|
||||
@@ -14,8 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const childProcess = require('child_process');
|
||||
const { spawn } = childProcess;
|
||||
const { spawn, execFile: execFileCb } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/;
|
||||
|
||||
@@ -36,24 +38,35 @@ function spawnPiped(cmd, options) {
|
||||
...options,
|
||||
});
|
||||
child.on('error', handleError);
|
||||
child.on('exit', code => {
|
||||
if (code) {
|
||||
print(`Child '${cmd.join(' ')}' exited with code ${code}`);
|
||||
process.exit(code);
|
||||
}
|
||||
});
|
||||
|
||||
const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' ');
|
||||
child.stdout.on(
|
||||
'data',
|
||||
pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `),
|
||||
pipeWithPrefix(process.stdout, `[${logPrefix}].out: `),
|
||||
);
|
||||
child.stderr.on(
|
||||
'data',
|
||||
pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `),
|
||||
pipeWithPrefix(process.stderr, `[${logPrefix}].err: `),
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
async function runPlain(cmd, options) {
|
||||
try {
|
||||
const { stdout } = await execFile(cmd[0], cmd.slice(1), {
|
||||
...options,
|
||||
shell: true,
|
||||
});
|
||||
return stdout.trim();
|
||||
} catch (error) {
|
||||
if (error.stderr) {
|
||||
process.stderr.write(error.stderr);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
process.stdout.write(`${err.name}: ${err.stack || err.message}\n`);
|
||||
if (typeof err.code === 'number') {
|
||||
@@ -133,7 +146,7 @@ async function waitForPageWithText(
|
||||
if (findTextAttempts <= maxFindTextAttempts) {
|
||||
await browser.visit(path);
|
||||
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
||||
continue
|
||||
continue;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -147,6 +160,7 @@ function print(msg) {
|
||||
|
||||
module.exports = {
|
||||
spawnPiped,
|
||||
runPlain,
|
||||
handleError,
|
||||
waitFor,
|
||||
waitForExit,
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"@types/webpack-dev-server": "^3.10.0",
|
||||
"del": "^5.1.0",
|
||||
"nodemon": "^2.0.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"ts-node": "^8.6.2",
|
||||
"zombie": "^6.1.4"
|
||||
},
|
||||
|
||||
@@ -14,18 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const normalizeBaseURL = (baseURL: string): string => {
|
||||
const url = new URL(baseURL);
|
||||
url.pathname = url.pathname.replace(/([^/])$/, '$1/');
|
||||
return url.toString();
|
||||
import fs from 'fs-extra';
|
||||
import { Command } from 'commander';
|
||||
import { createDistWorkspace } from '../lib/packager';
|
||||
|
||||
export default async (dir: string, _cmd: Command, packages: string[]) => {
|
||||
if (!(await fs.pathExists(dir))) {
|
||||
throw new Error(`Target workspace directory doesn't exist, '${dir}'`);
|
||||
}
|
||||
|
||||
await createDistWorkspace(packages, {
|
||||
targetDir: dir,
|
||||
});
|
||||
};
|
||||
|
||||
export default class URLParser {
|
||||
constructor(public baseURL: string, public pathname: string) {
|
||||
this.baseURL = normalizeBaseURL(baseURL);
|
||||
}
|
||||
|
||||
parse(): string {
|
||||
return new URL(this.pathname, this.baseURL).toString();
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,15 @@
|
||||
import fs from 'fs-extra';
|
||||
import { promisify } from 'util';
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import inquirer, { Answers, Question } from 'inquirer';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import os from 'os';
|
||||
import { Task, templatingTask, installWithLocalDeps } from '../../lib/tasks';
|
||||
import { Task, templatingTask } from '../../lib/tasks';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { version } from '../../lib/version';
|
||||
|
||||
const exec = promisify(execCb);
|
||||
|
||||
async function checkExists(rootDir: string, name: string) {
|
||||
@@ -39,7 +41,7 @@ async function checkExists(rootDir: string, name: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTemporaryAppFolder(tempDir: string) {
|
||||
async function createTemporaryAppFolder(tempDir: string) {
|
||||
await Task.forItem('creating', 'temporary directory', async () => {
|
||||
try {
|
||||
await fs.mkdir(tempDir);
|
||||
@@ -70,16 +72,12 @@ async function buildApp(appDir: string) {
|
||||
});
|
||||
};
|
||||
|
||||
await installWithLocalDeps(appDir);
|
||||
await runCmd('yarn install');
|
||||
await runCmd('yarn tsc');
|
||||
await runCmd('yarn build');
|
||||
}
|
||||
|
||||
export async function moveApp(
|
||||
tempDir: string,
|
||||
destination: string,
|
||||
id: string,
|
||||
) {
|
||||
async function moveApp(tempDir: string, destination: string, id: string) {
|
||||
await Task.forItem('moving', id, async () => {
|
||||
await fs.move(tempDir, destination).catch(error => {
|
||||
throw new Error(
|
||||
@@ -89,7 +87,7 @@ export async function moveApp(
|
||||
});
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
export default async (cmd: Command): Promise<void> => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -129,8 +127,10 @@ export default async () => {
|
||||
Task.section('Moving to final location');
|
||||
await moveApp(tempDir, appDir, answers.name);
|
||||
|
||||
Task.section('Building the app');
|
||||
await buildApp(appDir);
|
||||
if (!cmd.skipInstall) {
|
||||
Task.section('Building the app');
|
||||
await buildApp(appDir);
|
||||
}
|
||||
|
||||
Task.log();
|
||||
Task.log(
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
} from '../../lib/codeowners';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { version } from '../../lib/version';
|
||||
import { Task, templatingTask, installWithLocalDeps } from '../../lib/tasks';
|
||||
import { Task, templatingTask } from '../../lib/tasks';
|
||||
|
||||
const exec = promisify(execCb);
|
||||
|
||||
async function checkExists(rootDir: string, id: string) {
|
||||
@@ -141,9 +142,7 @@ async function cleanUp(tempDir: string) {
|
||||
}
|
||||
|
||||
async function buildPlugin(pluginFolder: string) {
|
||||
await installWithLocalDeps(paths.targetRoot);
|
||||
|
||||
const commands = ['yarn tsc', 'yarn build'];
|
||||
const commands = ['yarn install', 'yarn tsc', 'yarn build'];
|
||||
for (const command of commands) {
|
||||
await Task.forItem('executing', command, async () => {
|
||||
process.chdir(pluginFolder);
|
||||
|
||||
@@ -25,6 +25,10 @@ const main = (argv: string[]) => {
|
||||
program
|
||||
.command('create-app')
|
||||
.description('Creates a new app in a new directory')
|
||||
.option(
|
||||
'--skip-install',
|
||||
'Skip the install and builds steps after creating the app',
|
||||
)
|
||||
.action(
|
||||
lazyAction(() => import('./commands/create-app/createApp'), 'default'),
|
||||
);
|
||||
@@ -140,6 +144,11 @@ const main = (argv: string[]) => {
|
||||
.description('Delete cache directories')
|
||||
.action(lazyAction(() => import('./commands/clean/clean'), 'default'));
|
||||
|
||||
program
|
||||
.command('build-workspace <workspace-dir> ...<packages>')
|
||||
.description('Builds a temporary dist workspace from the provided packages')
|
||||
.action(lazyAction(() => import('./commands/buildWorkspace'), 'default'));
|
||||
|
||||
program.on('command:*', () => {
|
||||
console.log();
|
||||
console.log(
|
||||
|
||||
@@ -59,7 +59,7 @@ type Options = {
|
||||
*/
|
||||
export async function createDistWorkspace(
|
||||
packageNames: string[],
|
||||
options: Options,
|
||||
options: Options = {},
|
||||
) {
|
||||
const targetDir =
|
||||
options.targetDir ??
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { ExitCodeError } from './errors';
|
||||
import { promisify } from 'util';
|
||||
import { LogFunc } from './logging';
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
type SpawnOptionsPartialEnv = Omit<SpawnOptions, 'env'> & {
|
||||
|
||||
@@ -18,12 +18,8 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import handlebars from 'handlebars';
|
||||
import ora from 'ora';
|
||||
import { resolve as resolvePath, basename, dirname } from 'path';
|
||||
import { basename, dirname } from 'path';
|
||||
import recursive from 'recursive-readdir';
|
||||
import { promisify } from 'util';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { paths } from './paths';
|
||||
const exec = promisify(execCb);
|
||||
|
||||
const TASK_NAME_MAX_LENGTH = 14;
|
||||
|
||||
@@ -107,104 +103,3 @@ export async function templatingTask(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List of local packages that we need to modify as a part of an E2E test
|
||||
const PATCH_PACKAGES = [
|
||||
'cli',
|
||||
'config',
|
||||
'config-loader',
|
||||
'core',
|
||||
'core-api',
|
||||
'dev-utils',
|
||||
'test-utils',
|
||||
'test-utils-core',
|
||||
'theme',
|
||||
];
|
||||
|
||||
// This runs a `yarn install` task, but with special treatment for e2e tests
|
||||
export async function installWithLocalDeps(dir: string) {
|
||||
// This makes us install any package inside this repo as a local file dependency.
|
||||
// For example, instead of trying to fetch @backstage/core from npm, we point it
|
||||
// to <repo-root>/packages/core. This makes yarn use a simple file copy to install it instead.
|
||||
if (process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
Task.section('Linking packages locally for e2e tests');
|
||||
|
||||
const pkgJsonPath = resolvePath(dir, 'package.json');
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
|
||||
pkgJson.resolutions = pkgJson.resolutions || {};
|
||||
pkgJson.dependencies = pkgJson.dependencies || {};
|
||||
|
||||
if (!pkgJson.resolutions[`@backstage/${PATCH_PACKAGES[0]}`]) {
|
||||
for (const name of PATCH_PACKAGES) {
|
||||
await Task.forItem(
|
||||
'adding',
|
||||
`@backstage/${name} link to package.json`,
|
||||
async () => {
|
||||
const pkgPath = paths.resolveOwnRoot('packages', name);
|
||||
// Add to both resolutions and dependencies, or transitive dependencies will still be fetched from the registry.
|
||||
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
delete pkgJson.devDependencies[`@backstage/${name}`];
|
||||
|
||||
await fs
|
||||
.writeJSON(pkgJsonPath, pkgJson, { encoding: 'utf8', spaces: 2 })
|
||||
.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to add resolutions to package.json: ${error.message}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.forItem('executing', 'yarn install', async () => {
|
||||
await exec('yarn install', { cwd: dir }).catch(error => {
|
||||
process.stdout.write(error.stderr);
|
||||
process.stdout.write(error.stdout);
|
||||
throw new Error(
|
||||
`Could not execute command ${chalk.cyan('yarn install')}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// This takes care of pointing all the installed packages from this repo to
|
||||
// dist instead of the local src, using the field overrides in publishConfig.
|
||||
// Without this we get type checking errors in the e2e test
|
||||
if (process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
Task.section('Patching local dependencies for e2e tests');
|
||||
|
||||
for (const name of PATCH_PACKAGES) {
|
||||
await Task.forItem(
|
||||
'patching',
|
||||
`node_modules/@backstage/${name} package.json`,
|
||||
async () => {
|
||||
const depJsonPath = resolvePath(
|
||||
dir,
|
||||
'node_modules/@backstage',
|
||||
name,
|
||||
'package.json',
|
||||
);
|
||||
const depJson = await fs.readJson(depJsonPath);
|
||||
|
||||
// We want dist to be used for e2e tests
|
||||
for (const key of Object.keys(depJson.publishConfig)) {
|
||||
if (key !== 'access') {
|
||||
depJson[key] = depJson.publishConfig[key];
|
||||
}
|
||||
}
|
||||
|
||||
await fs
|
||||
.writeJSON(depJsonPath, depJson, { encoding: 'utf8', spaces: 2 })
|
||||
.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to add resolutions to package.json: ${error.message}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
"@backstage/core": "^{{version}}",
|
||||
"@backstage/test-utils": "^{{version}}",
|
||||
"@backstage/theme": "^{{version}}",
|
||||
"history": "^5.0.0",
|
||||
"plugin-welcome": "0.0.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-alpha.5",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^14.2.0",
|
||||
"react-router-dom": "6.0.0-alpha.5"
|
||||
"react-router-dom": "6.0.0-beta.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^{{version}}",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@types/react": "^16.9",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
|
||||
@@ -263,3 +263,13 @@ export const gitlabAuthApiRef = createApiRef<
|
||||
id: 'core.auth.gitlab',
|
||||
description: 'Provides authentication towards Gitlab APIs',
|
||||
});
|
||||
|
||||
/**
|
||||
* Provides authentication for custom identity providers.
|
||||
*/
|
||||
export const oauth2ApiRef = createApiRef<
|
||||
OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi
|
||||
>({
|
||||
id: 'core.auth.oauth2',
|
||||
description: 'Example of how to use oauth2 custom provider',
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './google';
|
||||
export * from './github';
|
||||
export * from './gitlab';
|
||||
export * from './google';
|
||||
export * from './oauth2';
|
||||
export * from './okta';
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import OAuth2Icon from '@material-ui/icons/AcUnit';
|
||||
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { Observable } from '../../../../types';
|
||||
import { AuthProvider, OAuthRequestApi } from '../../../definitions';
|
||||
import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentity,
|
||||
OAuthApi,
|
||||
OpenIdConnectApi,
|
||||
ProfileInfo,
|
||||
ProfileInfoApi,
|
||||
SessionState,
|
||||
SessionStateApi,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuth2Session } from './types';
|
||||
|
||||
type CreateOptions = {
|
||||
apiOrigin: string;
|
||||
basePath: string;
|
||||
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
|
||||
environment?: string;
|
||||
provider?: AuthProvider & { id: string };
|
||||
};
|
||||
|
||||
export type OAuth2Response = {
|
||||
providerInfo: {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
scope: string;
|
||||
expiresInSeconds: number;
|
||||
};
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: BackstageIdentity;
|
||||
};
|
||||
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'oauth2',
|
||||
title: 'Your Identity Provider',
|
||||
icon: OAuth2Icon,
|
||||
};
|
||||
|
||||
const SCOPE_PREFIX = '';
|
||||
|
||||
class OAuth2
|
||||
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
|
||||
static create({
|
||||
apiOrigin,
|
||||
basePath,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
apiOrigin,
|
||||
basePath,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
sessionTransform(res: OAuth2Response): OAuth2Session {
|
||||
return {
|
||||
...res,
|
||||
providerInfo: {
|
||||
idToken: res.providerInfo.idToken,
|
||||
accessToken: res.providerInfo.accessToken,
|
||||
scopes: OAuth2.normalizeScopes(res.providerInfo.scope),
|
||||
expiresAt: new Date(
|
||||
Date.now() + res.providerInfo.expiresInSeconds * 1000,
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const sessionManager = new RefreshingAuthSessionManager({
|
||||
connector,
|
||||
defaultScopes: new Set([
|
||||
'openid',
|
||||
`${SCOPE_PREFIX}userinfo.email`,
|
||||
`${SCOPE_PREFIX}userinfo.profile`,
|
||||
]),
|
||||
sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes,
|
||||
sessionShouldRefresh: (session: OAuth2Session) => {
|
||||
const expiresInSec =
|
||||
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
|
||||
return expiresInSec < 60 * 5;
|
||||
},
|
||||
});
|
||||
|
||||
return new OAuth2(sessionManager);
|
||||
}
|
||||
|
||||
sessionState$(): Observable<SessionState> {
|
||||
return this.sessionManager.sessionState$();
|
||||
}
|
||||
|
||||
constructor(private readonly sessionManager: SessionManager<OAuth2Session>) {}
|
||||
|
||||
async getAccessToken(
|
||||
scope?: string | string[],
|
||||
options?: AuthRequestOptions,
|
||||
) {
|
||||
const normalizedScopes = OAuth2.normalizeScopes(scope);
|
||||
const session = await this.sessionManager.getSession({
|
||||
...options,
|
||||
scopes: normalizedScopes,
|
||||
});
|
||||
return session?.providerInfo.accessToken ?? '';
|
||||
}
|
||||
|
||||
async getIdToken(options: AuthRequestOptions = {}) {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
return session?.providerInfo.idToken ?? '';
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.sessionManager.removeSession();
|
||||
}
|
||||
|
||||
async getBackstageIdentity(
|
||||
options: AuthRequestOptions = {},
|
||||
): Promise<BackstageIdentity | undefined> {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
return session?.backstageIdentity;
|
||||
}
|
||||
|
||||
async getProfile(options: AuthRequestOptions = {}) {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
return session?.profile;
|
||||
}
|
||||
|
||||
static normalizeScopes(scopes?: string | string[]): Set<string> {
|
||||
if (!scopes) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const scopeList = Array.isArray(scopes)
|
||||
? scopes
|
||||
: scopes.split(/[\s]/).filter(Boolean);
|
||||
|
||||
return new Set(scopeList);
|
||||
}
|
||||
}
|
||||
|
||||
export default OAuth2;
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default as OAuth2 } from './OAuth2';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ProfileInfo, BackstageIdentity } from '../../../definitions';
|
||||
|
||||
export type OAuth2Session = {
|
||||
providerInfo: {
|
||||
idToken: string;
|
||||
accessToken: string;
|
||||
scopes: Set<string>;
|
||||
expiresAt: Date;
|
||||
};
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: BackstageIdentity;
|
||||
};
|
||||
@@ -16,4 +16,5 @@
|
||||
|
||||
export * from './public';
|
||||
import * as privateExports from './private';
|
||||
|
||||
export default privateExports;
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
"react-dom": "^16.12.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-hook-form": "^5.7.2",
|
||||
"react-router": "6.0.0-alpha.5",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-sparklines": "^1.7.0",
|
||||
"react-syntax-highlighter": "^12.2.1",
|
||||
"react-use": "^14.2.0"
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
WithStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
|
||||
const tableTitleCellStyles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
|
||||
@@ -44,6 +44,9 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
'& > *': {
|
||||
flexShrink: 0,
|
||||
},
|
||||
},
|
||||
drawerOpen: {
|
||||
width: sidebarConfig.drawerWidthOpen,
|
||||
|
||||
@@ -14,25 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import SignOutIcon from '@material-ui/icons/MeetingRoom';
|
||||
import { SidebarContext } from './config';
|
||||
import {
|
||||
googleAuthApiRef,
|
||||
githubAuthApiRef,
|
||||
gitlabAuthApiRef,
|
||||
googleAuthApiRef,
|
||||
identityApiRef,
|
||||
oauth2ApiRef,
|
||||
oktaAuthApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-api';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import SignOutIcon from '@material-ui/icons/MeetingRoom';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { SidebarContext } from './config';
|
||||
import { SidebarItem } from './Items';
|
||||
import {
|
||||
OAuthProviderSettings,
|
||||
OIDCProviderSettings,
|
||||
UserProfile as SidebarUserProfile,
|
||||
} from './Settings';
|
||||
import { SidebarItem } from './Items';
|
||||
|
||||
export function SidebarUserSettings() {
|
||||
const { isOpen: sidebarOpen } = useContext(SidebarContext);
|
||||
@@ -68,6 +69,11 @@ export function SidebarUserSettings() {
|
||||
apiRef={oktaAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
<OIDCProviderSettings
|
||||
title="YourOrg"
|
||||
apiRef={oauth2ApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={SignOutIcon}
|
||||
text="Sign Out"
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5"
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import {
|
||||
ApiRegistry,
|
||||
AlertApiForwarder,
|
||||
alertApiRef,
|
||||
ApiRegistry,
|
||||
ErrorAlerter,
|
||||
ErrorApiForwarder,
|
||||
errorApiRef,
|
||||
GithubAuth,
|
||||
githubAuthApiRef,
|
||||
GitlabAuth,
|
||||
gitlabAuthApiRef,
|
||||
GoogleAuth,
|
||||
googleAuthApiRef,
|
||||
identityApiRef,
|
||||
OAuth2,
|
||||
oauth2ApiRef,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
googleAuthApiRef,
|
||||
githubAuthApiRef,
|
||||
gitlabAuthApiRef,
|
||||
oktaAuthApiRef,
|
||||
AlertApiForwarder,
|
||||
ErrorApiForwarder,
|
||||
ErrorAlerter,
|
||||
GoogleAuth,
|
||||
GithubAuth,
|
||||
GitlabAuth,
|
||||
OktaAuth,
|
||||
identityApiRef,
|
||||
oktaAuthApiRef,
|
||||
} from '@backstage/core';
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
@@ -72,4 +74,13 @@ builder.add(
|
||||
}),
|
||||
);
|
||||
|
||||
builder.add(
|
||||
oauth2ApiRef,
|
||||
OAuth2.create({
|
||||
apiOrigin: 'http://localhost:7000',
|
||||
basePath: '/auth/',
|
||||
oauthRequestApi,
|
||||
}),
|
||||
);
|
||||
|
||||
export const apis = builder.build();
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
"@types/react": "^16.9",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -25,6 +25,7 @@ import privateExports, {
|
||||
import { RenderResult } from '@testing-library/react';
|
||||
import { renderWithEffects } from '@backstage/test-utils-core';
|
||||
import { createMockApiRegistry } from './mockApiRegistry';
|
||||
|
||||
const { PrivateAppImpl } = privateExports;
|
||||
|
||||
const NotFoundErrorPage = () => {
|
||||
|
||||
@@ -15,14 +15,15 @@
|
||||
*/
|
||||
|
||||
import Router from 'express-promise-router';
|
||||
import { createGithubProvider } from './github';
|
||||
import { createGoogleProvider } from './google';
|
||||
import { createGitlabProvider } from './gitlab';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { createOktaProvider } from './okta';
|
||||
import { AuthProviderFactory, AuthProviderConfig } from './types';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { createGithubProvider } from './github';
|
||||
import { createGitlabProvider } from './gitlab';
|
||||
import { createGoogleProvider } from './google';
|
||||
import { createOAuth2Provider } from './oauth2';
|
||||
import { createOktaProvider } from './okta';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { AuthProviderConfig, AuthProviderFactory } from './types';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
@@ -30,6 +31,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
gitlab: createGitlabProvider,
|
||||
saml: createSamlProvider,
|
||||
okta: createOktaProvider,
|
||||
oauth2: createOAuth2Provider,
|
||||
};
|
||||
|
||||
export const createAuthProviderRouter = (
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createOAuth2Provider } from './provider';
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import { Strategy as OAuth2Strategy } from 'passport-oauth2';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
GenericOAuth2ProviderConfig,
|
||||
GenericOAuth2ProviderOptions,
|
||||
OAuthProviderHandlers,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
RedirectInfo,
|
||||
} from '../types';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export class OAuth2AuthProvider implements OAuthProviderHandlers {
|
||||
private readonly _strategy: OAuth2Strategy;
|
||||
|
||||
constructor(options: GenericOAuth2ProviderOptions) {
|
||||
this._strategy = new OAuth2Strategy(
|
||||
{ ...options, passReqToCallback: false as true },
|
||||
(
|
||||
accessToken: any,
|
||||
refreshToken: any,
|
||||
params: any,
|
||||
rawProfile: passport.Profile,
|
||||
done: PassportDoneCallback<OAuthResponse, PrivateInfo>,
|
||||
) => {
|
||||
const profile = makeProfileInfo(rawProfile, params.id_token);
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
providerInfo: {
|
||||
idToken: params.id_token,
|
||||
accessToken,
|
||||
scope: params.scope,
|
||||
expiresInSeconds: params.expires_in,
|
||||
},
|
||||
profile,
|
||||
},
|
||||
{
|
||||
refreshToken,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>,
|
||||
): Promise<RedirectInfo> {
|
||||
const providerOptions = {
|
||||
...options,
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
};
|
||||
return await executeRedirectStrategy(req, this._strategy, providerOptions);
|
||||
}
|
||||
|
||||
async handler(
|
||||
req: express.Request,
|
||||
): Promise<{ response: OAuthResponse; refreshToken: string }> {
|
||||
const { response, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OAuthResponse,
|
||||
PrivateInfo
|
||||
>(req, this._strategy);
|
||||
|
||||
return {
|
||||
response: await this.populateIdentity(response),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
|
||||
const { accessToken, params } = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
refreshToken,
|
||||
scope,
|
||||
);
|
||||
|
||||
const profile = await executeFetchUserProfileStrategy(
|
||||
this._strategy,
|
||||
accessToken,
|
||||
params.id_token,
|
||||
);
|
||||
|
||||
return this.populateIdentity({
|
||||
providerInfo: {
|
||||
accessToken,
|
||||
idToken: params.id_token,
|
||||
expiresInSeconds: params.expires_in,
|
||||
scope: params.scope,
|
||||
},
|
||||
profile,
|
||||
});
|
||||
}
|
||||
|
||||
// Use this function to grab the user profile info from the token
|
||||
// Then populate the profile with it
|
||||
private async populateIdentity(
|
||||
response: OAuthResponse,
|
||||
): Promise<OAuthResponse> {
|
||||
const { profile } = response;
|
||||
|
||||
if (!profile.email) {
|
||||
throw new Error('Profile does not contain a profile');
|
||||
}
|
||||
|
||||
const id = profile.email.split('@')[0];
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
}
|
||||
}
|
||||
|
||||
export function createOAuth2Provider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as GenericOAuth2ProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const callbackURLParam = `?env=${env}`;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/oauth2/handler/frame${callbackURLParam}`,
|
||||
authorizationURL: config.authorizationURL,
|
||||
tokenURL: config.tokenURL,
|
||||
};
|
||||
|
||||
if (
|
||||
!opts.clientID ||
|
||||
!opts.clientSecret ||
|
||||
!opts.authorizationURL ||
|
||||
!opts.tokenURL
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new OAuth2AuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId: 'oauth2',
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
return new EnvironmentHandler(envProviders);
|
||||
}
|
||||
@@ -33,6 +33,11 @@ export type OAuthProviderOptions = {
|
||||
callbackURL: string;
|
||||
};
|
||||
|
||||
export type GenericOAuth2ProviderOptions = OAuthProviderOptions & {
|
||||
authorizationURL: string;
|
||||
tokenURL: string;
|
||||
};
|
||||
|
||||
export type OAuthProviderConfig = {
|
||||
/**
|
||||
* Cookies can be marked with a secure flag to send cookies only when the request
|
||||
@@ -61,6 +66,11 @@ export type OAuthProviderConfig = {
|
||||
audience?: string;
|
||||
};
|
||||
|
||||
export type GenericOAuth2ProviderConfig = OAuthProviderConfig & {
|
||||
authorizationURL: string;
|
||||
tokenURL: string;
|
||||
};
|
||||
|
||||
export type EnvironmentProviderConfig = {
|
||||
/**
|
||||
* key, values are environment names and OAuthProviderConfigs
|
||||
|
||||
@@ -100,6 +100,16 @@ export async function createRouter(
|
||||
audience: process.env.AUTH_OKTA_AUDIENCE,
|
||||
},
|
||||
},
|
||||
oauth2: {
|
||||
development: {
|
||||
appOrigin: 'http://localhost:3000',
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_OAUTH2_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_OAUTH2_CLIENT_SECRET!,
|
||||
authorizationURL: process.env.AUTH_OAUTH2_AUTH_URL!,
|
||||
tokenURL: process.env.AUTH_OAUTH2_TOKEN_URL!,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
export {};
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
export {};
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0",
|
||||
"swr": "^0.2.2"
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { CatalogClient } from './CatalogClient';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
describe('CatalogClient', () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ContentHeader,
|
||||
identityApiRef,
|
||||
SupportButton,
|
||||
configApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
|
||||
@@ -51,6 +52,8 @@ const CatalogPageContents = () => {
|
||||
const userId = useApi(identityApiRef).getUserId();
|
||||
const [selectedTab, setSelectedTab] = useState<string>();
|
||||
const [selectedSidebarItem, setSelectedSidebarItem] = useState<string>();
|
||||
const orgName =
|
||||
useApi(configApiRef).getOptionalString('organization.name') ?? 'Company';
|
||||
|
||||
const tabs = useMemo<LabeledComponentType[]>(
|
||||
() => [
|
||||
@@ -98,7 +101,7 @@ const CatalogPageContents = () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Company', // TODO: Replace with Company name, read from app config.
|
||||
name: orgName,
|
||||
items: [
|
||||
{
|
||||
id: 'all',
|
||||
@@ -108,7 +111,7 @@ const CatalogPageContents = () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
[isStarredEntity, userId],
|
||||
[isStarredEntity, userId, orgName],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,14 +18,16 @@ import { Table, TableColumn, TableProps } from '@backstage/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
import GitHub from '@material-ui/icons/GitHub';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarOutline from '@material-ui/icons/StarBorder';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
import { findLocationForEntityMeta } from '../../data/utils';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
import { entityRoute } from '../../routes';
|
||||
import {
|
||||
favouriteEntityIcon,
|
||||
favouriteEntityTooltip,
|
||||
} from '../FavouriteEntity/FavouriteEntity';
|
||||
|
||||
const columns: TableColumn<Entity>[] = [
|
||||
{
|
||||
@@ -125,13 +127,8 @@ export const CatalogTable = ({
|
||||
const isStarred = isStarredEntity(rowData);
|
||||
return {
|
||||
cellStyle: { paddingLeft: '1em' },
|
||||
icon: () =>
|
||||
isStarred ? (
|
||||
<Star htmlColor="#f3ba37" fontSize="small" />
|
||||
) : (
|
||||
<StarOutline fontSize="small" />
|
||||
),
|
||||
tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites',
|
||||
icon: () => favouriteEntityIcon(isStarred),
|
||||
tooltip: favouriteEntityTooltip(isStarred),
|
||||
onClick: () => toggleStarredEntity(rowData),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { SentryIssuesWidget } from '@backstage/plugin-sentry';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { Grid, Box } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -37,6 +37,7 @@ import { catalogApiRef } from '../..';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard';
|
||||
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
|
||||
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
|
||||
|
||||
const REDIRECT_DELAY = 1000;
|
||||
function headerProps(
|
||||
@@ -63,6 +64,16 @@ export const getPageTheme = (entity?: Entity): PageTheme => {
|
||||
return pageTheme[themeKey] ?? pageTheme.home;
|
||||
};
|
||||
|
||||
const EntityPageTitle: FC<{ title: string; entity: Entity | undefined }> = ({
|
||||
entity,
|
||||
title,
|
||||
}) => (
|
||||
<Box display="inline-flex" alignItems="center" height="1em">
|
||||
{title}
|
||||
{entity && <FavouriteEntity entity={entity} />}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const EntityPage: FC<{}> = () => {
|
||||
const { optionalNamespaceAndName, kind } = useParams() as {
|
||||
optionalNamespaceAndName: string;
|
||||
@@ -138,7 +149,11 @@ export const EntityPage: FC<{}> = () => {
|
||||
|
||||
return (
|
||||
<Page theme={getPageTheme(entity)}>
|
||||
<Header title={headerTitle} type={headerType}>
|
||||
<Header
|
||||
title={<EntityPageTitle title={headerTitle} entity={entity} />}
|
||||
pageTitleOverride={headerTitle}
|
||||
type={headerType}
|
||||
>
|
||||
{entity && (
|
||||
<>
|
||||
<HeaderLabel
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { IconButton, Tooltip, withStyles } from '@material-ui/core';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
type Props = ComponentProps<typeof IconButton> & { entity: Entity };
|
||||
|
||||
const YellowStar = withStyles({
|
||||
root: {
|
||||
color: '#f3ba37',
|
||||
},
|
||||
})(Star);
|
||||
|
||||
export const favouriteEntityTooltip = (isStarred: boolean) =>
|
||||
isStarred ? 'Remove from favorites' : 'Add to favorites';
|
||||
|
||||
export const favouriteEntityIcon = (isStarred: boolean) =>
|
||||
isStarred ? <YellowStar /> : <StarBorder />;
|
||||
|
||||
/**
|
||||
* IconButton for showing if a current entity is starred and adding/removing it from the favourite entities
|
||||
* @param props MaterialUI IconButton props extended by required `entity` prop
|
||||
*/
|
||||
export const FavouriteEntity: React.FC<Props> = props => {
|
||||
const { toggleStarredEntity, isStarredEntity } = useStarredEntities();
|
||||
const isStarred = isStarredEntity(props.entity);
|
||||
return (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
{...props}
|
||||
onClick={() => toggleStarredEntity(props.entity)}
|
||||
>
|
||||
<Tooltip title={favouriteEntityTooltip(isStarred)}>
|
||||
{favouriteEntityIcon(isStarred)}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -40,8 +40,8 @@
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-lazylog": "^4.5.2",
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import React, { FC, useReducer, Dispatch, Reducer } from 'react';
|
||||
import { circleCIApiRef } from '../api';
|
||||
import type { State, Action, SettingsState } from './types';
|
||||
|
||||
export type { SettingsState };
|
||||
|
||||
export const AppContext = React.createContext<[State, Dispatch<Action>]>(
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
# github-actions
|
||||
|
||||
Welcome to the github-actions plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
|
||||
## Getting started
|
||||
|
||||
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/github-actions](http://localhost:3000/github-actions).
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { plugin } from '../src/plugin';
|
||||
|
||||
createDevApp().registerPlugin(plugin).render();
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@backstage/plugin-github-actions",
|
||||
"version": "0.1.1-alpha.12",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.12",
|
||||
"@backstage/theme": "^0.1.1-alpha.12",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.12",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.12",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/node": "^12.0.0",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Build, BuildDetails, BuildStatus } from './types';
|
||||
|
||||
export class BuildsClient {
|
||||
static create(): BuildsClient {
|
||||
return new BuildsClient();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async listBuilds(_entityUri: string): Promise<Build[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getBuild(_buildUri: string): Promise<BuildDetails> {
|
||||
return {
|
||||
build: {
|
||||
commitId: 'TODO',
|
||||
branch: 'TODO',
|
||||
uri: 'TODO',
|
||||
status: BuildStatus.Running,
|
||||
message: 'TODO',
|
||||
},
|
||||
author: 'TODO',
|
||||
logUrl: 'TODO',
|
||||
overviewUrl: 'TODO',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildsClient } from './BuildsClient';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum BuildStatus {
|
||||
Null,
|
||||
Success,
|
||||
Failure,
|
||||
Pending,
|
||||
Running,
|
||||
}
|
||||
|
||||
export type Build = {
|
||||
commitId: string;
|
||||
message: string;
|
||||
branch: string;
|
||||
status: BuildStatus;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
export type BuildDetails = {
|
||||
build: Build;
|
||||
author: string;
|
||||
logUrl: string;
|
||||
overviewUrl: string;
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link } from '@backstage/core';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { BuildStatusIndicator } from '../BuildStatusIndicator';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
maxWidth: 720,
|
||||
margin: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
padding: theme.spacing(1, 0, 2, 0),
|
||||
},
|
||||
table: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const client = BuildsClient.create();
|
||||
|
||||
export const BuildDetailsPage = () => {
|
||||
const classes = useStyles();
|
||||
const { buildUri } = useParams();
|
||||
const status = useAsync(() => client.getBuild(buildUri), [buildUri]);
|
||||
|
||||
if (status.loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
return (
|
||||
<Typography variant="h6" color="error">
|
||||
Failed to load build, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const details = status.value;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography className={classes.title} variant="h3">
|
||||
<Link to="/builds">
|
||||
<Typography component="span" variant="h3" color="primary">
|
||||
<
|
||||
</Typography>
|
||||
</Link>
|
||||
Build Details
|
||||
</Typography>
|
||||
<TableContainer component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Branch</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.build.branch}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.build.message}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.build.commitId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={details?.build.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Author</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.author}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Links</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ButtonGroup
|
||||
variant="text"
|
||||
color="primary"
|
||||
aria-label="text primary button group"
|
||||
>
|
||||
{details?.overviewUrl && (
|
||||
<Button>
|
||||
<Link to={details.overviewUrl}>GitHub</Link>
|
||||
</Button>
|
||||
)}
|
||||
{details?.logUrl && (
|
||||
<Button>
|
||||
<Link to={details.logUrl}>Logs</Link>
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildDetailsPage } from './BuildDetailsPage';
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link } from '@backstage/core';
|
||||
import {
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { BuildStatusIndicator } from '../BuildStatusIndicator';
|
||||
|
||||
const client = BuildsClient.create();
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
// height: 400,
|
||||
},
|
||||
title: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const BuildInfoCard = () => {
|
||||
const classes = useStyles();
|
||||
const status = useAsync(() => client.listBuilds('entity:spotify:backstage'));
|
||||
|
||||
let content: JSX.Element;
|
||||
|
||||
if (status.loading) {
|
||||
content = <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
content = (
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
const [build] =
|
||||
status.value?.filter(({ branch }) => branch === 'master') ?? [];
|
||||
|
||||
content = (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
|
||||
<Typography color="primary">{build?.message}</Typography>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{build?.commitId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={build?.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h2" className={classes.title}>
|
||||
Master Build
|
||||
</Typography>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildInfoCard } from './BuildInfoCard';
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link } from '@backstage/core';
|
||||
import {
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Theme,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { BuildStatusIndicator } from '../BuildStatusIndicator';
|
||||
|
||||
const client = BuildsClient.create();
|
||||
|
||||
const LongText = ({ text, max }: { text: string; max: number }) => {
|
||||
if (text.length < max) {
|
||||
return <span>{text}</span>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={text}>
|
||||
<span>{text.slice(0, max)}...</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
padding: theme.spacing(1, 0, 2, 0),
|
||||
},
|
||||
}));
|
||||
|
||||
const PageContents = () => {
|
||||
const { loading, error, value } = useAsync(() =>
|
||||
client.listBuilds('entity:spotify:backstage'),
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <LinearProgress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {error.message}{' '}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="CI/CD builds table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Branch</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
<TableCell>Commit</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{value!.map(build => (
|
||||
<TableRow key={build.uri}>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={build.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography>
|
||||
<LongText text={build.branch} max={30} />
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`builds/${encodeURIComponent(build.uri)}`}>
|
||||
<Typography color="primary">
|
||||
<LongText text={build.message} max={60} />
|
||||
</Typography>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={build.commitId}>
|
||||
<Typography noWrap>{build.commitId.slice(0, 10)}</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const BuildListPage = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h3" className={classes.title}>
|
||||
CI/CD Builds
|
||||
</Typography>
|
||||
<PageContents />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildListPage } from './BuildListPage';
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconComponent } from '@backstage/core';
|
||||
import { makeStyles, Theme } from '@material-ui/core';
|
||||
import ProgressIcon from '@material-ui/icons/Autorenew';
|
||||
import SuccessIcon from '@material-ui/icons/CheckCircle';
|
||||
import FailureIcon from '@material-ui/icons/Error';
|
||||
import UnknownIcon from '@material-ui/icons/Help';
|
||||
import React from 'react';
|
||||
import { BuildStatus } from '../../apis/builds';
|
||||
|
||||
type Props = {
|
||||
status?: BuildStatus;
|
||||
};
|
||||
|
||||
type StatusStyle = {
|
||||
icon: IconComponent;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const styles: { [key in BuildStatus]: StatusStyle } = {
|
||||
[BuildStatus.Null]: {
|
||||
icon: UnknownIcon,
|
||||
color: '#f49b20',
|
||||
},
|
||||
[BuildStatus.Success]: {
|
||||
icon: SuccessIcon,
|
||||
color: '#1db855',
|
||||
},
|
||||
[BuildStatus.Failure]: {
|
||||
icon: FailureIcon,
|
||||
color: '#CA001B',
|
||||
},
|
||||
[BuildStatus.Pending]: {
|
||||
icon: UnknownIcon,
|
||||
color: '#5BC0DE',
|
||||
},
|
||||
[BuildStatus.Running]: {
|
||||
icon: ProgressIcon,
|
||||
color: '#BEBEBE',
|
||||
},
|
||||
};
|
||||
|
||||
const useStyles = makeStyles<Theme, StatusStyle>({
|
||||
icon: style => ({
|
||||
color: style.color,
|
||||
}),
|
||||
});
|
||||
|
||||
export const BuildStatusIndicator = ({ status }: Props) => {
|
||||
const style = (status && styles[status]) || styles[BuildStatus.Null];
|
||||
const classes = useStyles(style);
|
||||
const Icon = style.icon;
|
||||
|
||||
return (
|
||||
<div className={classes.icon}>
|
||||
<Icon />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildStatusIndicator } from './BuildStatusIndicator';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { plugin } from './plugin';
|
||||
|
||||
describe('github-actions', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import { BuildDetailsPage } from './components/BuildDetailsPage';
|
||||
import { BuildListPage } from './components/BuildListPage';
|
||||
|
||||
// TODO(freben): This is just a demo route for now
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/github-actions',
|
||||
title: 'GitHub Actions',
|
||||
});
|
||||
export const buildRouteRef = createRouteRef({
|
||||
path: '/github-actions/builds/:buildUri',
|
||||
title: 'GitHub Actions Build',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'github-actions',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, BuildListPage);
|
||||
router.addRoute(buildRouteRef, BuildDetailsPage);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
@@ -28,7 +28,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
|
||||