Merge branch 'master' into blam/options
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
###
|
||||
# This will download all Homebrew dependencies.
|
||||
###
|
||||
|
||||
# Shared
|
||||
brew "git"
|
||||
brew "docker"
|
||||
brew "docker-compose"
|
||||
brew "protobuf"
|
||||
brew "prototool"
|
||||
cask "docker"
|
||||
|
||||
# Frontend
|
||||
brew "node@12"
|
||||
brew "yarn"
|
||||
|
||||
# Backend
|
||||
brew "go"
|
||||
@@ -1,14 +1,35 @@
|
||||
###
|
||||
# All-in-one build command.
|
||||
# All-in-one commands
|
||||
###
|
||||
build: build-yarn-dependencies build-protocol-definitions
|
||||
init: init-secrets
|
||||
install: install-homebrew-dependencies install-yarn-dependencies install-forego
|
||||
start: build-protocol-definitions
|
||||
forego start
|
||||
|
||||
###
|
||||
# Setup secrets
|
||||
###
|
||||
init-secrets:
|
||||
cp secrets.env.example secrets.env
|
||||
|
||||
###
|
||||
# Install any Homebrew dependencies specified in Brewfile
|
||||
###
|
||||
install-homebrew-dependencies:
|
||||
brew bundle
|
||||
|
||||
###
|
||||
# Download dependencies from the Frontend using Yarn.
|
||||
###
|
||||
build-yarn-dependencies:
|
||||
install-yarn-dependencies:
|
||||
yarn --cwd ${PWD}/frontend install
|
||||
|
||||
###
|
||||
# Install Forego for running both frontend and backend in a single command.
|
||||
###
|
||||
install-forego:
|
||||
go get -u github.com/ddollar/forego
|
||||
|
||||
###
|
||||
# Protobuf Definitions.
|
||||
# This will generate Protobuf definitions from ./proto to both Go and JS/TypeScript.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
backend: docker-compose up --build
|
||||
frontend: cd frontend/ && yarn install && yarn start
|
||||
@@ -8,12 +8,22 @@ Backstage is an open platform for building developer portals.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Install Dependencies with Homebrew and Yarn
|
||||
|
||||
Run the following to install relevant dependencies (such as Git, Docker, etc):
|
||||
|
||||
```bash
|
||||
# If you don't have Homebrew, run the following command:
|
||||
# $ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
$ make install
|
||||
```
|
||||
|
||||
### Secrets
|
||||
|
||||
To setup secrets, copy the `secrets.env.example` to `secrets.env` as such:
|
||||
|
||||
```bash
|
||||
$ cp secrets.env.example secrets.env
|
||||
$ make init-secrets
|
||||
```
|
||||
|
||||
### Protobuf Definitions
|
||||
@@ -28,27 +38,10 @@ See [proto/README.md](proto/README.md) for more information.
|
||||
|
||||
## Running Locally
|
||||
|
||||
First step is to set up a `secrets.env` file in the root of the repo. Use the following template but fill in your own values:
|
||||
Once you've installed all dependencies, start serving the frontend using `yarn`:
|
||||
|
||||
```bash
|
||||
# Github Access token with repo scope, created at https://github.com/settings/tokens
|
||||
BOSS_GH_ACCESS_TOKEN=<access-token>
|
||||
```
|
||||
|
||||
Then run start all backend services using `docker-compose`:
|
||||
|
||||
```bash
|
||||
$ ./docker-compose.yaml up --build
|
||||
```
|
||||
|
||||
And finally install all dependencies and start serving the frontend using `yarn`:
|
||||
|
||||
```bash
|
||||
$ cd frontend
|
||||
|
||||
$ yarn install
|
||||
|
||||
$ yarn start
|
||||
$ make start
|
||||
```
|
||||
|
||||
## Plugins
|
||||
|
||||
+2
-1
@@ -31,5 +31,6 @@ CMD ["./service"]
|
||||
### Scaffolder Stage
|
||||
### needs extra cookiecutter
|
||||
FROM default AS scaffolder
|
||||
RUN apt-get update && apt-get install -y python-pip
|
||||
RUN apt-get update && apt-get install -y python-pip
|
||||
RUN apt-get install -y python-backports.functools-lru-cache
|
||||
RUN pip install cookiecutter==1.7.0 --index-url https://pypi.python.org/simple
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spotify/backstage/inventory/storage"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Config struct holds the current configuration
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Address string
|
||||
Port int
|
||||
}
|
||||
|
||||
Logging struct {
|
||||
Format string
|
||||
Level string
|
||||
}
|
||||
|
||||
DB storage.Config
|
||||
}
|
||||
|
||||
// Initialize a new Config
|
||||
func Initialize(configFile string) *Config {
|
||||
cfg := DefaultConfig()
|
||||
ReadConfigFile(cfg, getConfigFilePath(configFile))
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// DefaultConfig returns a Config struct with default values
|
||||
func DefaultConfig() *Config {
|
||||
cfg := &Config{}
|
||||
|
||||
cfg.Server.Address = "0.0.0.0"
|
||||
cfg.Server.Port = 50051
|
||||
|
||||
cfg.Logging.Format = "text"
|
||||
cfg.Logging.Level = "DEBUG"
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getConfigFilePath(configPath string) string {
|
||||
if configPath != "" {
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return configPath
|
||||
}
|
||||
panic(fmt.Sprintf("unable to open %s.", configPath))
|
||||
}
|
||||
path, _ := osext.ExecutableFolder()
|
||||
path = fmt.Sprintf("%s/config.toml", path)
|
||||
if _, err := os.Open(path); err == nil {
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ReadConfigFile(cfg *Config, path string) {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := toml.DecodeFile(path, cfg); err != nil {
|
||||
logrus.WithError(err).Fatal("unable to read config")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ go 1.12
|
||||
replace github.com/spotify/backstage/proto => ../proto
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/golang/protobuf v1.3.3
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spotify/backstage/proto v0.0.0-00010101000000-000000000000
|
||||
go.etcd.io/bbolt v1.3.3
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -12,7 +14,15 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -32,6 +42,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
Binary file not shown.
@@ -1,25 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/spotify/backstage/inventory/config"
|
||||
"github.com/spotify/backstage/inventory/storage"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/spotify/backstage/inventory/app"
|
||||
pb "github.com/spotify/backstage/proto/inventory/v1"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
port = ":50051"
|
||||
)
|
||||
|
||||
func main() {
|
||||
lis, err := net.Listen("tcp", port)
|
||||
configFile := flag.String("config", "", "specify a config.toml file")
|
||||
flag.Parse()
|
||||
go catchInterrupt()
|
||||
|
||||
cfg := config.Initialize(*configFile)
|
||||
setupLogging(cfg)
|
||||
storage := getStorage(cfg)
|
||||
|
||||
address := fmt.Sprintf("%v:%v", cfg.Server.Address, cfg.Server.Port)
|
||||
lis, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
log.Infof("inventory service listening on: %v", address)
|
||||
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterInventoryServer(grpcServer, &app.Server{})
|
||||
pb.RegisterInventoryServer(grpcServer, &app.Server{Storage: storage})
|
||||
grpcServer.Serve(lis)
|
||||
}
|
||||
|
||||
func getStorage(cfg *config.Config) *storage.Storage {
|
||||
storage, err := storage.OpenStorage(cfg.DB)
|
||||
if err != nil {
|
||||
log.Fatalf("could not open database: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
func setupLogging(cfg *config.Config) {
|
||||
if cfg.Logging.Format == "json" {
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
FieldMap: log.FieldMap{
|
||||
log.FieldKeyLevel: "severity",
|
||||
},
|
||||
})
|
||||
}
|
||||
level, err := log.ParseLevel(cfg.Logging.Level)
|
||||
if err != nil {
|
||||
level = log.InfoLevel
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetLevel(level)
|
||||
}
|
||||
|
||||
func catchInterrupt() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, os.Kill)
|
||||
s := <-c
|
||||
if s != os.Interrupt && s != os.Kill {
|
||||
return
|
||||
}
|
||||
log.Info("shutting down...")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
BackstageTheme,
|
||||
createApp,
|
||||
InfoCard,
|
||||
} from '@backstage/core';
|
||||
import { BackstageTheme, createApp, InfoCard } from '@backstage/core';
|
||||
//import PageHeader from './components/PageHeader';
|
||||
import { LoginComponent } from '@backstage/plugin-login';
|
||||
import HomePagePlugin from '@backstage/plugin-home-page';
|
||||
@@ -24,6 +20,7 @@ const useStyles = makeStyles(theme => ({
|
||||
body: {
|
||||
height: '100%',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
'overscroll-behavior-y': 'none',
|
||||
},
|
||||
a: {
|
||||
color: 'inherit',
|
||||
|
||||
@@ -152,7 +152,7 @@ const useStyles = makeStyles(theme => ({
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
width: theme.spacing(7) + 1,
|
||||
height: '100vh',
|
||||
height: '100%',
|
||||
},
|
||||
drawer: {
|
||||
display: 'flex',
|
||||
|
||||
@@ -17,8 +17,8 @@ import GithubActionsPlugin from '@backstage/plugin-github-actions';
|
||||
|
||||
/* SERVICE */
|
||||
const serviceOverviewPage = createWidgetView()
|
||||
.addComponent(MockEntityCard)
|
||||
.addComponent(MockEntityCard);
|
||||
.add({ size: 4, component: MockEntityCard })
|
||||
.register(GithubActionsPlugin);
|
||||
|
||||
const serviceView = createEntityPage()
|
||||
.addPage('Overview', WebIcon, '/overview', serviceOverviewPage)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { PluginOutput, RoutePath, RouteOptions } from './types';
|
||||
import { IconComponent } from '../types';
|
||||
import { Widget } from '../widgetView/types';
|
||||
|
||||
export type PluginConfig = {
|
||||
id: string;
|
||||
@@ -10,6 +11,7 @@ export type PluginConfig = {
|
||||
export type PluginHooks = {
|
||||
router: RouterHooks;
|
||||
entityPage: EntityPageHooks;
|
||||
widgets: WidgetHooks;
|
||||
};
|
||||
|
||||
export type RouterHooks = {
|
||||
@@ -41,6 +43,10 @@ export type EntityPageHooks = {
|
||||
): void;
|
||||
};
|
||||
|
||||
export type WidgetHooks = {
|
||||
add(widget: Widget): void;
|
||||
};
|
||||
|
||||
export const registerSymbol = Symbol('plugin-register');
|
||||
export const outputSymbol = Symbol('plugin-output');
|
||||
|
||||
@@ -98,6 +104,11 @@ export default class Plugin {
|
||||
});
|
||||
},
|
||||
},
|
||||
widgets: {
|
||||
add(widget: Widget) {
|
||||
outputs.push({ type: 'widget', widget });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.storedOutput = outputs;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { IconComponent } from '../types';
|
||||
import { Widget } from '../widgetView/types';
|
||||
|
||||
export type RouteOptions = {
|
||||
// Whether the route path must match exactly, defaults to true.
|
||||
@@ -22,6 +23,11 @@ export type RedirectRouteOutput = {
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type WidgetOutput = {
|
||||
type: 'widget';
|
||||
widget: Widget;
|
||||
};
|
||||
|
||||
export type EntityPageViewRouteOutput = {
|
||||
type: 'entity-page-view-route';
|
||||
path: RoutePath;
|
||||
@@ -39,5 +45,6 @@ export type EntityPageNavItemOutput = {
|
||||
export type PluginOutput =
|
||||
| RouteOutput
|
||||
| RedirectRouteOutput
|
||||
| WidgetOutput
|
||||
| EntityPageViewRouteOutput
|
||||
| EntityPageNavItemOutput;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type IconComponent = ComponentType<{
|
||||
fontSize: 'inherit' | 'default' | 'small' | 'large';
|
||||
fontSize?: 'inherit' | 'default' | 'small' | 'large';
|
||||
}>;
|
||||
|
||||
@@ -1,50 +1,65 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { App, AppComponentBuilder } from '../app/types';
|
||||
import { Widget } from './types';
|
||||
import BackstagePlugin from '../plugin/Plugin';
|
||||
import DefaultWidgetView from '../../components/DefaultWidgetView';
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
cards: ComponentType<any>[];
|
||||
};
|
||||
|
||||
const WidgetViewComponent: FC<Props> = ({ cards }) => {
|
||||
return (
|
||||
<div>
|
||||
{cards.map((CardComponent, index) => (
|
||||
<CardComponent key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type WidgetViewRegistration = {
|
||||
type: 'component';
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
type WidgetViewRegistration =
|
||||
| {
|
||||
type: 'component';
|
||||
widget: Widget;
|
||||
}
|
||||
| {
|
||||
type: 'plugin';
|
||||
plugin: BackstagePlugin;
|
||||
};
|
||||
|
||||
export default class WidgetViewBuilder extends AppComponentBuilder {
|
||||
private readonly registrations = new Array<WidgetViewRegistration>();
|
||||
private output?: ComponentType<any>;
|
||||
|
||||
addComponent(component: ComponentType<any>): WidgetViewBuilder {
|
||||
this.registrations.push({ type: 'component', component });
|
||||
add(widget: Widget): WidgetViewBuilder {
|
||||
this.registrations.push({ type: 'component', widget });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(app: App): ComponentType<any> {
|
||||
register(plugin: BackstagePlugin): WidgetViewBuilder {
|
||||
this.registrations.push({ type: 'plugin', plugin });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(_app: App): ComponentType<any> {
|
||||
if (this.output) {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
const cards = this.registrations.map(reg => {
|
||||
const widgets = new Array<Widget>();
|
||||
|
||||
for (const reg of this.registrations) {
|
||||
switch (reg.type) {
|
||||
case 'component':
|
||||
return reg.component;
|
||||
widgets.push(reg.widget);
|
||||
break;
|
||||
case 'plugin':
|
||||
let added = false;
|
||||
for (const output of reg.plugin.output()) {
|
||||
if (output.type === 'widget') {
|
||||
widgets.push(output.widget);
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
throw new Error(
|
||||
`Plugin ${reg.plugin} was registered as widget provider, but did not provide any widgets`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown WidgetViewBuilder registration`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.output = () => <WidgetViewComponent app={app} cards={cards} />;
|
||||
this.output = () => <DefaultWidgetView widgets={widgets} />;
|
||||
return this.output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type Widget = {
|
||||
size: 4 | 6 | 8 | 12;
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
export type WidgetViewProps = {
|
||||
widgets: Widget[];
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { Switch, Route, Redirect } from 'react-router-dom';
|
||||
import DefaultEntityPageHeader from '../DefaultEntityPageHeader';
|
||||
import DefaultEntityPageNavbar from '../DefaultEntityPageNavbar';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
const useStyles = makeStyles<Theme>({
|
||||
root: {
|
||||
display: 'grid',
|
||||
gridTemplateAreas: `
|
||||
@@ -16,7 +16,6 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
gridTemplateColumns: 'auto 1fr',
|
||||
minHeight: '100%',
|
||||
paddingBottom: theme.spacing(3),
|
||||
},
|
||||
header: {
|
||||
gridArea: 'header',
|
||||
@@ -27,7 +26,7 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
content: {
|
||||
gridArea: 'content',
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const DefaultEntityPage: FC<EntityPageProps> = ({ navItems, views }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ const useStyles = makeStyles<Theme>({
|
||||
transitionTimingFunction: 'ease-in',
|
||||
backgroundColor: '#eeeeee',
|
||||
boxShadow: '0px 0 4px 0px rgba(0,0,0,0.35)',
|
||||
height: '100%',
|
||||
},
|
||||
list: {
|
||||
padding: 0,
|
||||
|
||||
@@ -15,7 +15,6 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
borderBottom: `1px solid #d9d9d9`,
|
||||
paddingLeft: theme.spacing(1),
|
||||
},
|
||||
label: {
|
||||
color: '#333',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Grid, Paper, makeStyles, Theme } from '@material-ui/core';
|
||||
import { WidgetViewProps } from '../../api/widgetView/types';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
widgetWrapper: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const WidgetViewComponent: FC<WidgetViewProps> = ({ widgets }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Grid container direction="row" spacing={2}>
|
||||
{widgets.map(({ size, component: WidgetComponent }, index) => (
|
||||
<Grid key={index} item xs={size}>
|
||||
<Paper className={classes.widgetWrapper}>
|
||||
<WidgetComponent />
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetViewComponent;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DefaultWidgetView';
|
||||
@@ -18,7 +18,8 @@
|
||||
"react-dom": "^16.12.0",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"formik": "2.1.4"
|
||||
"formik": "2.1.4",
|
||||
"formik-material-ui": "2.0.0-alpha.3"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "web-scripts lint",
|
||||
|
||||
+27
-6
@@ -3,6 +3,9 @@ import { useRouteMatch } from 'react-router-dom';
|
||||
import { useFormik } from 'formik';
|
||||
import { Button, TextField, makeStyles } from '@material-ui/core';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import { scaffolderV1 } from '@backstage/protobuf-definitions';
|
||||
const google_protobuf_struct_pb = require('google-protobuf/google/protobuf/struct_pb.js');
|
||||
// import * as google_protobuf_struct_pb from 'google-protobuf/google/protobuf/struct_pb.js';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
formGroup: {
|
||||
@@ -21,7 +24,21 @@ const CreateEntityFormPage = () => {
|
||||
description: '',
|
||||
},
|
||||
onSubmit: (values: any) => {
|
||||
alert(JSON.stringify(values, null, 2));
|
||||
const client = new scaffolderV1.Client('http://localhost:8080');
|
||||
const req = new scaffolderV1.CreateRequest();
|
||||
req.setComponentId(values.entityId);
|
||||
req.setTemplateId(templateId);
|
||||
|
||||
req.setMetadata(
|
||||
new google_protobuf_struct_pb.Struct.fromJavaScript({
|
||||
description: values.description,
|
||||
}),
|
||||
);
|
||||
req.setPrivate(false);
|
||||
client.create(req).then((res: scaffolderV1.CreateReply) => {
|
||||
console.log('COMPONENT CREATED');
|
||||
console.log(res.toObject().componentId);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,9 +49,9 @@ const CreateEntityFormPage = () => {
|
||||
<div className={classes.formGroup}>
|
||||
<TextField
|
||||
label="Entity Id:"
|
||||
name="entity-id"
|
||||
id="entity-id"
|
||||
value={formik.values.entityId}
|
||||
name="entityId"
|
||||
id="entityId"
|
||||
onChange={formik.handleChange}
|
||||
variant="outlined"
|
||||
></TextField>
|
||||
</div>
|
||||
@@ -43,12 +60,16 @@ const CreateEntityFormPage = () => {
|
||||
label="Description:"
|
||||
name="description"
|
||||
id="description"
|
||||
value={formik.values.description}
|
||||
onChange={formik.handleChange}
|
||||
variant="outlined"
|
||||
></TextField>
|
||||
</div>
|
||||
<div className={classes.formGroup}>
|
||||
<Button variant="contained" color="primary">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={formik.submitForm}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+79
-57
@@ -15,13 +15,23 @@ import {
|
||||
makeStyles,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import { RelativeEntityLink } from '@backstage/core';
|
||||
import BuildStatusIndicator from '../BuildStatusIndicator';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
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),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {};
|
||||
|
||||
@@ -47,61 +57,73 @@ const BuildDetailsPage: FC<Props> = () => {
|
||||
const details = status.value;
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper} className={classes.root}>
|
||||
<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>{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"
|
||||
>
|
||||
<Button>
|
||||
<Link href={details?.overviewUrl}>GitHub</Link>
|
||||
</Button>
|
||||
<Button>
|
||||
<Link href={details?.logUrl}>Logs</Link>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<div className={classes.root}>
|
||||
<Typography className={classes.title} variant="h3">
|
||||
<RelativeEntityLink view="/builds">
|
||||
<Typography component={'span'} variant="h3" color="primary">
|
||||
<{' '}
|
||||
</Typography>
|
||||
</RelativeEntityLink>
|
||||
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"
|
||||
>
|
||||
<Button>
|
||||
<Link href={details?.overviewUrl}>GitHub</Link>
|
||||
</Button>
|
||||
<Button>
|
||||
<Link href={details?.logUrl}>Logs</Link>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+84
-2
@@ -1,8 +1,90 @@
|
||||
import React, { FC } from 'react';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import { RelativeEntityLink } from '@backstage/core';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { useAsync } from 'react-use';
|
||||
import {
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import BuildStatusIndicator from '../BuildStatusIndicator';
|
||||
|
||||
const client = BuildsClient.create('http://localhost:8080');
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
// height: 400,
|
||||
},
|
||||
title: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const BuildInfoCard: FC<{}> = () => {
|
||||
return <InfoCard>Last build was acb67fa3b5472w</InfoCard>;
|
||||
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>
|
||||
<RelativeEntityLink
|
||||
view={`builds/${encodeURIComponent(build?.uri || '')}`}
|
||||
>
|
||||
<Typography color="primary">{build?.message}</Typography>
|
||||
</RelativeEntityLink>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildInfoCard;
|
||||
|
||||
+7
-5
@@ -16,6 +16,7 @@ import {
|
||||
import { RelativeEntityLink } from '@backstage/core';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { useAsync } from 'react-use';
|
||||
import BuildStatusIndicator from '../BuildStatusIndicator';
|
||||
|
||||
const client = BuildsClient.create('http://localhost:8080');
|
||||
|
||||
@@ -35,7 +36,7 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
paddingBottom: theme.spacing(2),
|
||||
padding: theme.spacing(1, 0, 2, 0),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -49,7 +50,7 @@ const BuildListPage: FC<{}> = () => {
|
||||
content = <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
content = (
|
||||
<Typography variant="h4" color="error">
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
@@ -68,8 +69,9 @@ const BuildListPage: FC<{}> = () => {
|
||||
<TableBody>
|
||||
{status.value!.map(build => (
|
||||
<TableRow key={build.uri}>
|
||||
{/* TODO: make this an indicating blobby thing */}
|
||||
<TableCell>{build.status}</TableCell>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={build.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography>
|
||||
<LongText text={build.branch} max={30} />
|
||||
@@ -101,7 +103,7 @@ const BuildListPage: FC<{}> = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h4" className={classes.title}>
|
||||
<Typography variant="h3" className={classes.title}>
|
||||
CI/CD Builds
|
||||
</Typography>
|
||||
{content}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import React, { FC } from 'react';
|
||||
import { makeStyles, Theme } from '@material-ui/core';
|
||||
import { BuildStatus } from '../../apis/builds';
|
||||
import FailureIcon from '@material-ui/icons/Error';
|
||||
import SuccessIcon from '@material-ui/icons/CheckCircle';
|
||||
import ProgressIcon from '@material-ui/icons/Autorenew';
|
||||
import UnknownIcon from '@material-ui/icons/Help';
|
||||
import { IconComponent } from '@backstage/core';
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
||||
const BuildStatusIndicator: FC<Props> = props => {
|
||||
const { status } = props;
|
||||
const style = (status && styles[status]) || styles[BuildStatus.Null];
|
||||
|
||||
const classes = useStyles(style);
|
||||
|
||||
const IconComponent = style.icon;
|
||||
|
||||
return (
|
||||
<div className={classes.icon}>
|
||||
<IconComponent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildStatusIndicator;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './BuildStatusIndicator';
|
||||
@@ -2,6 +2,7 @@ import { createPlugin } from '@backstage/core';
|
||||
import BuildDetailsPage from './components/BuildDetailsPage';
|
||||
import BuildListPage from './components/BuildListPage';
|
||||
import BuildIcon from '@material-ui/icons/Build';
|
||||
import BuildInfoCard from './components/BuildInfoCard';
|
||||
|
||||
// export const buildListRoute = createEntityRoute<[]>('/builds')
|
||||
// export const buildDetailsRoute = createEntityRoute<[number]>('/builds/:buildId')
|
||||
@@ -9,9 +10,10 @@ import BuildIcon from '@material-ui/icons/Build';
|
||||
export default createPlugin({
|
||||
id: 'github-actions',
|
||||
|
||||
register({ entityPage }) {
|
||||
register({ entityPage, widgets }) {
|
||||
entityPage.navItem({ title: 'CI/CD', icon: BuildIcon, target: '/builds' });
|
||||
entityPage.route('/builds', BuildListPage);
|
||||
entityPage.route('/builds/:buildUri', BuildDetailsPage);
|
||||
widgets.add({ size: 8, component: BuildInfoCard });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
overflowY: 'auto',
|
||||
},
|
||||
pageBody: {
|
||||
padding: theme.spacing(2),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
avatarButton: {
|
||||
padding: theme.spacing(2),
|
||||
@@ -27,7 +27,7 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
const HomePage: FC<{}> = () => {
|
||||
const classes = useStyles();
|
||||
const columns = [
|
||||
{ id: 'id', label: 'ID' },
|
||||
{ id: 'entity', label: 'ID' },
|
||||
{ id: 'kind', label: 'Kind' },
|
||||
];
|
||||
|
||||
@@ -36,7 +36,8 @@ const HomePage: FC<{}> = () => {
|
||||
{ id: 'backstage-microsite', kind: 'website' },
|
||||
].map(({ id, kind }) => {
|
||||
return {
|
||||
id: (
|
||||
id,
|
||||
entity: (
|
||||
<EntityLink kind={kind} id={id}>
|
||||
<Typography color="primary">{id}</Typography>
|
||||
</EntityLink>
|
||||
@@ -52,15 +53,18 @@ const HomePage: FC<{}> = () => {
|
||||
<HomePageTimer />
|
||||
</Header>
|
||||
<div className={classes.pageBody}>
|
||||
<Grid container direction="column" spacing={6}>
|
||||
<Grid item xs={12}>
|
||||
<SquadTechHealth />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<InfoCard title="Stuff you own" maxWidth>
|
||||
<Grid container direction="row" spacing={3}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="h3" style={{ padding: '8px 0 16px 0' }}>
|
||||
Things you own
|
||||
</Typography>
|
||||
<InfoCard maxWidth>
|
||||
<SortableTable data={data} columns={columns} orderBy="id" />
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<SquadTechHealth />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,9 @@ import { HorizontalScrollGrid, ProgressCard } from '@backstage/core';
|
||||
const SquadTechHealth: FC<{}> = () => {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h3">Team Metrics</Typography>
|
||||
<Typography variant="h3" style={{ padding: '8px 0 16px 0' }}>
|
||||
Team Metrics
|
||||
</Typography>
|
||||
<HorizontalScrollGrid scrollStep={400} scrollSpeed={100}>
|
||||
<Grid item>
|
||||
<ProgressCard
|
||||
|
||||
@@ -59,7 +59,7 @@ const LoginComponent: FC<Props> = ({ onLogin }) => {
|
||||
Login
|
||||
</Button>
|
||||
</Grid>
|
||||
<Typography>{error || 'Just enter any fake username'}</Typography>
|
||||
<Typography>{error || 'Use your github username'}</Typography>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7127,6 +7127,11 @@ form-data@~2.3.2:
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formik-material-ui@2.0.0-alpha.3:
|
||||
version "2.0.0-alpha.3"
|
||||
resolved "https://registry.npmjs.org/formik-material-ui/-/formik-material-ui-2.0.0-alpha.3.tgz#1f5e4d98068686fa1c1c0cf54e75b51e92b14981"
|
||||
integrity sha512-jVG18/cFa89j9omVPQUSJ+4kK7mJ1Wxz0hPVUKwdX+Cr1Z4Vfl1pBo6tJ0xzS2cOj4NEN35D+9Ft1DPAsDa90g==
|
||||
|
||||
formik@2.1.4:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.npmjs.org/formik/-/formik-2.1.4.tgz#8deef07ec845ea98f75e03da4aad7aab4ac46570"
|
||||
|
||||
+15
-1
@@ -1,5 +1,19 @@
|
||||
###
|
||||
# The environment you want to run the frontend (running using create-react-app) in.
|
||||
# In production, these should be `production` and false respectively.
|
||||
###
|
||||
NODE_ENV=development
|
||||
NODE_DEBUG=true
|
||||
|
||||
###
|
||||
# This is the full HTTP endpoint to the GraphQL API you'll want to point to.
|
||||
# For example (in development): http://localhost:8080/graphql
|
||||
###
|
||||
REACT_APP_GRAPHQL_API=
|
||||
BOSS_GH_ACCESS_TOKEN=
|
||||
|
||||
###
|
||||
# A valid GitHub access token. You'll need to give it access to "repo".
|
||||
# To create one, visit https://github.com/settings/tokens in your browser.
|
||||
###
|
||||
BOSS_GH_USERNAME=
|
||||
BOSS_GH_ACCESS_TOKEN=
|
||||
|
||||
Reference in New Issue
Block a user