Teleport V5 (#185)

* New Top Nav (#136)
* Moves AuthConnectors/TrustedClusters/Roles to the Dashboard Top Nav
* Add feature to allow user in context to perform operations on other users (#137)
* Added List/Create/Update/Delete/Reset user functionality
* Added enterprise api endpoint to OSS config
Co-authored-by: Alexey Kontsevoy <biz.kovoy@gmail.com>

* Fix handling of feature flags (#138)
* Prevent closing when there is an error with deleting user (#142)
* Alexey/v5nav (#155)
* Touch Ups (#157)
* Create a story for Teleport App (#158)
* Add Application Launcher (AAP) (#162)
* use publicAddr and clusterName URL parameters to launch apps. (#169)
* Update sideNav styles and ssh input box (#170)
* [teleport] Implement add node dialog (#165)
* [teleport] Change NodeJoinToken prop expires to expiry (#176)
* Remove empty state from NodesList (#179)
* [teleport] Add -f flag to add node curl command (#180)
* [teleport] Add app session chunk and start events (#181)
* Add Application Dialog
* Delete Force
Co-authored-by: Lisa Kim <lisa@gravitational.com>
This commit is contained in:
Alexey Kontsevoy 2020-11-09 17:02:33 -05:00 committed by GitHub
parent 08e3c69712
commit 43d2a39a21
512 changed files with 18777 additions and 11741 deletions

1
web/.gitignore vendored
View file

@ -3,5 +3,4 @@ node_modules
coverage
dist
**/dist
packages/force/dist
!packages/gravity/dist

View file

@ -12,7 +12,6 @@ COPY packages/design/package.json web-apps/packages/design/
COPY packages/gravity/package.json web-apps/packages/gravity/
COPY packages/shared/package.json web-apps/packages/shared/
COPY packages/teleport/package.json web-apps/packages/teleport/
COPY packages/force/package.json web-apps/packages/force/
# copy enterprise package.json files if present
COPY README.md packages/webapps.e/gravity/package.jso[n] web-apps/packages/webapps.e/gravity/

View file

@ -19,10 +19,6 @@ build:
test:
$(MAKE) docker-build NPM_CMD=test
.PHONY: build-force
build-force:
$(MAKE) docker-build NPM_CMD=build-force FROM=dist/force/ TO=dist/force
.PHONY: build-gravity-oss
build-gravity-oss:
$(MAKE) docker-build NPM_CMD=build-gravity-oss FROM=dist/gravity/ TO=dist/gravity

View file

@ -3,7 +3,6 @@
This mono-repository contains the source code for the web UIs of the following projects:
[Teleport](https://github.com/gravitational/teleport)
[Gravity](https://github.com/gravitational/gravity)
[Force](https://github.com/gravitational/force)
The code is organized in terms of independent yarn packages which reside in
the [packages directory](https://github.com/gravitational/webapps/tree/master/packages).
@ -35,12 +34,6 @@ To build the Gravity web UI
$ yarn build-gravity
```
To build the Force web UI
```
$ yarn build-force
```
The resulting output will be in the `/packages/{package-name}/dist/` folders respectively.
### Docker Build
@ -63,12 +56,6 @@ To build the Gravity web UI
$ make packages/gravity/dist
```
To build the Force web UI
```
$ make packages/force/dist
```
## Development
To avoid having to install a dedicated Teleport or Gravity cluster,

View file

@ -8,19 +8,17 @@
"test": "jest",
"test-coverage": "jest --coverage && scripts/print-coverage-link.sh",
"tdd": "jest --watch",
"start-force": "yarn workspace @gravitational/force start",
"start-gravity": "yarn workspace @gravitational/gravity start",
"start-teleport": "yarn workspace @gravitational/teleport start",
"start-gravity-e": "yarn workspace @gravitational/gravity.e start",
"start-teleport-e": "yarn workspace @gravitational/teleport.e start",
"build-force": "yarn workspace @gravitational/force build --output-path=../../dist/force/app",
"build-gravity": "yarn build-gravity && yarn build-gravity-e",
"build-gravity-e": "yarn workspace @gravitational/gravity.e build --output-path=../../../dist/e/gravity/app",
"build-gravity-oss": "yarn workspace @gravitational/gravity build --output-path=../../dist/gravity/app",
"build-teleport": "yarn build-teleport-oss && yarn build-teleport-e",
"build-teleport-oss": "yarn workspace @gravitational/teleport build --output-path=../../dist/teleport/app",
"build-teleport-e": "yarn workspace @gravitational/teleport.e build --output-path=../../../dist/e/teleport/app",
"build-oss": "yarn build-force && yarn build-teleport-oss && yarn build-gravity-oss",
"build-oss": "yarn build-teleport-oss && yarn build-gravity-oss",
"build-e": "yarn build-teleport-e && yarn build-gravity-e",
"build": "yarn type-check && yarn build-oss && yarn build-e",
"type-check": "yarn tsc"

View file

@ -20,16 +20,15 @@ module.exports = {
moduleNameMapper: {
// mock all imports to asset files
'\\.(css|scss|stylesheet)$': path.join(__dirname, 'mockStyles.js'),
'\\.(png|svg)$': path.join(__dirname, 'mockFiles.js'),
'\\.(png|svg|yaml)$': path.join(__dirname, 'mockFiles.js'),
// Below aliases allow easier migration of gravitational code to this monorepo.
// They also give shorter names to gravitational packages.
jQuery: 'jquery',
'^shared/(.*)$': '<rootDir>/packages/shared/$1',
'^design($|/.*)': '<rootDir>/packages/design/src/$1',
'^gravity/(.*)$': '<rootDir>/packages/gravity/src/$1',
'^teleport/(.*)$': '<rootDir>/packages/teleport/src/$1',
'^teleport($|/.*)': '<rootDir>/packages/teleport/src/$1',
'^e-teleport/(.*)$': '<rootDir>/packages/webapps.e/teleport/src/$1',
'^e-shared/(.*)$': '<rootDir>/packages/webapps.e/shared/src/$1',
'^e-gravity/(.*)$': '<rootDir>/packages/webapps.e/gravity/src/$1',
},
};

View file

@ -62,7 +62,6 @@ module.exports = function createConfig() {
jQuery: 'jquery',
teleport: path.join(__dirname, '/../../teleport/src'),
'e-teleport': path.join(__dirname, '/../../webapps.e/teleport/src'),
'e-shared': path.join(__dirname, '/../../webapps.e/shared/src'),
'e-gravity': path.join(__dirname, '/../../webapps.e/gravity/src'),
design: path.join(__dirname, '/../../design/src'),
shared: path.join(__dirname, '/../../shared'),

View file

@ -77,7 +77,19 @@ export const StyledTable = styled.table(
tbody tr:hover {
background-color: ${darken(props.theme.colors.primary.lighter, 0.14)};
}`,
:last-child {
td:first-child {
border-bottom-left-radius: 8px;
}
td:last-child {
border-bottom-right-radius: 8px;
}
}
}
`,
space,
borderRadius
);

View file

@ -53,6 +53,7 @@ export const ArrowsVertical = makeFontIcon(
'icon-chevrons-expand-vertical'
);
export const ArrowUp = makeFontIcon('ArrowUp', 'icon-chevron-up');
export const AlarmRing = makeFontIcon('AlarmRing', 'icon-alarm-ringing');
export const BitBucket = makeFontIcon('Bitbucket', 'icon-bitbucket');
export const Bubble = makeFontIcon('Bubble', 'icon-bubble');
export const Camera = makeFontIcon('Camera', 'icon-camera');
@ -104,6 +105,7 @@ export const ClipboardUser = makeFontIcon(
export const Close = makeFontIcon('Close', 'icon-close');
export const Cloud = makeFontIcon('Cloud', 'icon-cloud');
export const Cluster = makeFontIcon('Cluster', 'icon-site-map');
export const Clusters = makeFontIcon('Clusters', 'icon-icons2');
export const ClusterAdded = makeFontIcon('ClusterAdded', 'icon-cluster-added');
export const ClusterAuth = makeFontIcon('ClusterAuth', 'icon-cluster-auth');
export const Code = makeFontIcon('Code', 'icon-code');
@ -128,6 +130,7 @@ export const Edit = makeFontIcon('Edit', 'icon-pencil4');
export const Ellipsis = makeFontIcon('Ellipsis', 'icon-ellipsis');
export const EmailSolid = makeFontIcon('EmailSolid', 'icon-email-solid');
export const Equalizer = makeFontIcon('Equalizer', 'icon-equalizer');
export const EqualizerVertical = makeFontIcon('EqualizerVertical', 'icon-equalizer1');
export const Expand = makeFontIcon('Expand', 'icon-frame-expand');
export const Facebook = makeFontIcon('Facebook', 'icon-facebook');
export const FacebookSquare = makeFontIcon('FacebookSquare', 'icon-facebook2');
@ -153,6 +156,7 @@ export const Link = makeFontIcon('Link', 'icon-link');
export const Linkedin = makeFontIcon('Linkedin', 'icon-linkedin');
export const Linux = makeFontIcon('Linux', 'icon-linux');
export const List = makeFontIcon('List', 'icon-list');
export const ListThin = makeFontIcon('ListThin', 'icon-list1');
export const ListAddCheck = makeFontIcon(
'ListAddCheck',
'icon-playlist_add_check'
@ -168,6 +172,7 @@ export const Memory = makeFontIcon('Memory', 'icon-memory');
export const MoreHoriz = makeFontIcon('MoreHoriz', 'icon-more_horiz');
export const MoreVert = makeFontIcon('MoreVert', 'icon-more_vert');
export const Mute = makeFontIcon('Mute', 'icon-mute');
export const NewTab = makeFontIcon('NewTab', 'icon-new-tab');
export const NoteAdded = makeFontIcon('NoteAdded', 'icon-note_add');
export const NotificationsActive = makeFontIcon(
'NotificationsActive',
@ -219,6 +224,7 @@ export const Spinner = makeFontIcon('Spinner', 'icon-spinner8');
export const Stars = makeFontIcon('Stars', 'icon-stars');
export const Stripe = makeFontIcon('Stripe', 'icon-cc-stripe');
export const Tablet = makeFontIcon('Tablet', 'icon-tablet2');
export const Terminal = makeFontIcon('Terminal', 'icon-cli');
export const Trash = makeFontIcon('Trash', 'icon-trash2');
export const Twitter = makeFontIcon('Twitter', 'icon-twitter');
export const Unarchive = makeFontIcon('Unarchive', 'icon-unarchive');
@ -231,6 +237,7 @@ export const VideoGame = makeFontIcon('VideoGame', 'icon-videogame_asset');
export const Visa = makeFontIcon('Visa', 'icon-cc-visa');
export const VolumeUp = makeFontIcon('VolumeUp', 'icon-volume-high');
export const VpnKey = makeFontIcon('VpnKey', 'icon-vpn_key');
export const Wand = makeFontIcon('Wand', 'icon-magic-wand');
export const Warning = makeFontIcon('Warning', 'icon-warning');
export const Wifi = makeFontIcon('Wifi', 'icon-wifi');
export const Windows = makeFontIcon('Windows', 'icon-windows');

View file

@ -37,6 +37,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.ArrowRight} text="ArrowRight" />
<IconBox IconCmpt={Icon.ArrowsVertical} text="ArrowsVertical" />
<IconBox IconCmpt={Icon.ArrowUp} text="ArrowUp" />
<IconBox IconCmpt={Icon.AlarmRing} text="AlarmRing" />
<IconBox IconCmpt={Icon.Bubble} text="Bubble" />
<IconBox IconCmpt={Icon.Camera} text="Camera" />
<IconBox IconCmpt={Icon.CardView} text="CardView" />
@ -68,6 +69,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.Close} text="Close" />
<IconBox IconCmpt={Icon.Cloud} text="Cloud" />
<IconBox IconCmpt={Icon.Cluster} text="Cluster" />
<IconBox IconCmpt={Icon.Clusters} text="Clusters" />
<IconBox IconCmpt={Icon.ClusterAdded} text="ClusterAdded" />
<IconBox IconCmpt={Icon.ClusterAuth} text="ClusterAuth" />
<IconBox IconCmpt={Icon.Code} text="Code" />
@ -86,6 +88,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.Ellipsis} text="Ellipsis" />
<IconBox IconCmpt={Icon.EmailSolid} text="EmailSolid" />
<IconBox IconCmpt={Icon.Equalizer} text="Equalizer" />
<IconBox IconCmpt={Icon.EqualizerVertical} text="EqualizerVertical" />
<IconBox IconCmpt={Icon.Expand} text="Expand" />
<IconBox IconCmpt={Icon.Facebook} text="Facebook" />
<IconBox IconCmpt={Icon.FacebookSquare} text="FacebookSquare" />
@ -109,6 +112,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.ListAddCheck} text="ListAddCheck" />
<IconBox IconCmpt={Icon.ListBullet} text="ListBullet" />
<IconBox IconCmpt={Icon.ListCheck} text="ListCheck" />
<IconBox IconCmpt={Icon.ListThin} text="ListThin" />
<IconBox IconCmpt={Icon.ListView} text="ListView" />
<IconBox IconCmpt={Icon.LocalPlay} text="LocalPlay" />
<IconBox IconCmpt={Icon.Lock} text="Lock" />
@ -118,6 +122,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.MoreHoriz} text="MoreHoriz" />
<IconBox IconCmpt={Icon.MoreVert} text="MoreVert" />
<IconBox IconCmpt={Icon.Mute} text="Mute" />
<IconBox IconCmpt={Icon.NewTab} text="NewTab" />
<IconBox IconCmpt={Icon.NoteAdded} text="NoteAdded" />
<IconBox IconCmpt={Icon.NotificationsActive} text="NotificationsActive" />
<IconBox IconCmpt={Icon.Paypal} text="Paypal" />
@ -150,6 +155,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.Stars} text="Stars" />
<IconBox IconCmpt={Icon.Stripe} text="Stripe" />
<IconBox IconCmpt={Icon.Tablet} text="Tablet" />
<IconBox IconCmpt={Icon.Terminal} text="Terminal" />
<IconBox IconCmpt={Icon.Trash} text="Trash" />
<IconBox IconCmpt={Icon.Twitter} text="Twitter" />
<IconBox IconCmpt={Icon.Unarchive} text="Unarchive" />
@ -163,6 +169,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.VolumeUp} text="VolumeUp" />
<IconBox IconCmpt={Icon.VpnKey} text="VpnKey" />
<IconBox IconCmpt={Icon.Warning} text="Warning" />
<IconBox IconCmpt={Icon.Wand} text="Wand" />
<IconBox IconCmpt={Icon.Wifi} text="Wifi" />
<IconBox IconCmpt={Icon.Windows} text="Windows" />
<IconBox IconCmpt={Icon.Youtube} text="Youtube" />

View file

@ -28,6 +28,7 @@ import Icon, {
ArrowRight,
ArrowsVertical,
ArrowUp,
AlarmRing,
BitBucket,
Bubble,
Camera,
@ -58,6 +59,7 @@ import Icon, {
Close,
Cloud,
Cluster,
Clusters,
ClusterAdded,
ClusterAuth,
Code,
@ -76,6 +78,7 @@ import Icon, {
Ellipsis,
EmailSolid,
Equalizer,
EqualizerVertical,
Expand,
Facebook,
FacebookSquare,
@ -101,6 +104,7 @@ import Icon, {
ListAddCheck,
ListBullet,
ListCheck,
ListThin,
ListView,
LocalPlay,
Lock,
@ -110,6 +114,7 @@ import Icon, {
MoreHoriz,
MoreVert,
Mute,
NewTab,
NoteAdded,
NotificationsActive,
OpenID,
@ -140,6 +145,7 @@ import Icon, {
Stars,
Stripe,
Tablet,
Terminal,
Trash,
Twitter,
Unarchive,
@ -152,6 +158,7 @@ import Icon, {
Visa,
VolumeUp,
VpnKey,
Wand,
Warning,
Wifi,
Windows,
@ -173,6 +180,7 @@ export {
ArrowRight,
ArrowsVertical,
ArrowUp,
AlarmRing,
BitBucket,
Bubble,
Camera,
@ -203,6 +211,7 @@ export {
Close,
Cloud,
Cluster,
Clusters,
ClusterAdded,
ClusterAuth,
Code,
@ -221,6 +230,7 @@ export {
Ellipsis,
EmailSolid,
Equalizer,
EqualizerVertical,
Expand,
Facebook,
FacebookSquare,
@ -246,6 +256,7 @@ export {
ListAddCheck,
ListBullet,
ListCheck,
ListThin,
ListView,
LocalPlay,
Lock,
@ -255,6 +266,7 @@ export {
MoreHoriz,
MoreVert,
Mute,
NewTab,
NoteAdded,
NotificationsActive,
OpenID,
@ -285,6 +297,7 @@ export {
Stars,
Stripe,
Tablet,
Terminal,
Trash,
Twitter,
Unarchive,
@ -297,6 +310,7 @@ export {
Visa,
VolumeUp,
VpnKey,
Wand,
Warning,
Wifi,
Windows,

View file

@ -33,6 +33,12 @@ const Image = props => {
Image.propTypes = {
/** Image Src */
src: PropTypes.string,
...space.propTypes,
...color.propTypes,
...width.propTypes,
...height.propTypes,
...maxWidth.propTypes,
...maxHeight.propTypes,
};
Image.displayName = 'Logo';

View file

@ -72,8 +72,8 @@ const StyledSpinner = styled(SpinnerIcon)`
width: ${fontSize};
`}
animation: anim-rotate 1s infinite linear;
color: #FFF;
animation: anim-rotate 2s infinite linear;
color: #fff;
display: inline-block;
margin: 16px;
opacity: 0.24;

View file

@ -50,6 +50,10 @@ const Input = styled.input`
opacity: 0.4;
}
:read-only {
cursor: not-allowed
}
${color} ${space} ${width} ${height} ${error};
`;

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import styled from 'styled-components';
import defaultTheme from 'design/theme';
import { space } from 'design/system';
function Link({ ...props }) {
return <StyledButtonLink {...props} />;
@ -39,6 +40,8 @@ const StyledButtonLink = styled.a`
&:focus {
background: ${({ theme }) => theme.colors.primary.light};
}
${space}
`;
export default Link;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 152 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -14,6 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* match searches for a match, given a search value, through an array of objects.
*
* @param obj An array of objects to search for matches
* @param searchValue The value to look for in obj
* @param searchableProps The properties in obj that we can match searchValue
* @param cb Callback function to handle special cases like data format differences.
* E.g: user sees date as '2020/01/15', but data is stored as 'Wed Jan 15 2020',
* we must first convert data as how user sees it, then apply match.
*
* cb(target: any[], searchValue: string, prop: string):
* - param target: to apply the searchValue against (find a match)
* - param searchValue: the value to look for in target
* - param prop: the current obj property name where searchValue may be matched
*/
export default function match(obj, searchValue, { searchableProps, cb }) {
searchValue = searchValue.toLocaleUpperCase();
let propNames = searchableProps || Object.getOwnPropertyNames(obj);

View file

@ -1 +0,0 @@
src/proto

View file

@ -1,11 +0,0 @@
const eslint = require('@gravitational/build/.eslintrc');
// a place for custom eslint rules
const custom = {
...eslint,
rules: {
...eslint.rules,
},
};
module.exports = custom;

View file

@ -1,35 +0,0 @@
# Gravitational Force Web UI
This package contains the source code of Force Web UI
## Build
```
$ yarn build
```
The dist files will be created in the `/packages/force/dist/` directory
## Development
If `https://example.com:3080/web` is your server URL then you can
start local development server:
```
$ yarn start --target=https://example.com:3080/web
```
### Typescript
To run a type check
```
$ yarn type-check
```
To run a type check in watch mode
```
$ yarn type-watch
```

View file

@ -1,38 +0,0 @@
{
"name": "@gravitational/force",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "g-start",
"build": "g-build --config webpack.config.js --progress --bail",
"test": "npx jest",
"tdd": "jest . --watch",
"type-check": "yarn tsc",
"type-watch": "yarn tsc --watch"
},
"author": "",
"license": "Apache-2.0",
"repository": {
"type" : "git",
"url" : "https://github.com/gravitational/webapps/webapps.git",
"directory": "packages/force"
},
"dependencies": {
"cross-env": "5.0.5",
"@gravitational/shared": "1.0.0",
"@gravitational/design": "1.0.0",
"@types/google-protobuf": "^3.7.2",
"@improbable-eng/grpc-web": "^0.12.0",
"ts-protoc-gen": "^0.12.0",
"typescript": "3.7.5",
"google-protobuf": "^3.11.4",
"string-replace-loader": "^2.3.0"
},
"devDependencies": {
"@gravitational/build": "^1.0.0",
"file-loader": "6.0.0",
"url-loader": "4.0.0",
"typescript": "3.7.5"
}
}

View file

@ -1,64 +0,0 @@
import React, { useState, useEffect } from 'react';
// Protobuf components
import { TickService } from '../../../proto/tick_pb_service';
import { TickRequest, Tick } from '../../../proto/tick_pb';
// GRPC
import { grpc } from '@improbable-eng/grpc-web';
const hostport = location.hostname + (location.port ? ':' + location.port : '');
// Ticker is a ticker function
export default function Ticker() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
const [tick, setTick] = useState('');
// Similar to componentDidMount and componentDidUpdate:
useEffect(
() => {
fetch('/api/ping')
.then(response => response.json())
.then(body => {
setTick(body);
});
const tickRequest = new TickRequest();
let request = grpc.invoke(TickService.Subscribe, {
request: tickRequest,
transport: grpc.WebsocketTransport(),
host: `https://${hostport}`,
onMessage: (tick: Tick) => {
setTick(new Date(tick.toObject().time / 1000000).toString());
window.console.log('got tick: ', tick.toObject());
},
onEnd: (
code: grpc.Code,
msg: string | undefined,
trailers: grpc.Metadata
) => {
if (code == grpc.Code.OK) {
window.console.log('all ok');
} else {
window.console.log('hit an error', code, msg, trailers);
}
},
});
// stops subscription stream once component unmounts
return () => {
request.close();
};
},
[] /* tells React that it should not depend on grpc*/
);
return (
<div>
<p>
You clicked {count} times, tick is {tick}
</p>
<button onClick={() => setCount(count + 1)}>Click me again</button>
</div>
);
}

View file

@ -1,2 +0,0 @@
import Ticker from './Ticker';
export default Ticker;

View file

@ -1,38 +0,0 @@
/*
Copyright 2019 Gravitational, Inc.
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 { hot } from 'react-hot-loader/root';
import { createBrowserHistory } from 'history';
import React from 'react';
import { Router } from 'react-router';
import { Route, Switch } from 'react-router-dom';
import ThemeProvider from 'design/ThemeProvider';
import Dashboard from './components/Dashboard';
const browserHistory = createBrowserHistory();
function Force(): JSX.Element {
return (
<ThemeProvider>
<Router history={browserHistory}>
<Switch>
<Route path="/" component={Dashboard} />
</Switch>
</Router>
</ThemeProvider>
);
}
export default hot(Force);

View file

@ -1,41 +0,0 @@
// package: proto
// file: tick.proto
import * as jspb from "google-protobuf";
export class Tick extends jspb.Message {
getTime(): number;
setTime(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Tick.AsObject;
static toObject(includeInstance: boolean, msg: Tick): Tick.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Tick, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Tick;
static deserializeBinaryFromReader(message: Tick, reader: jspb.BinaryReader): Tick;
}
export namespace Tick {
export type AsObject = {
time: number,
}
}
export class TickRequest extends jspb.Message {
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): TickRequest.AsObject;
static toObject(includeInstance: boolean, msg: TickRequest): TickRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: TickRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): TickRequest;
static deserializeBinaryFromReader(message: TickRequest, reader: jspb.BinaryReader): TickRequest;
}
export namespace TickRequest {
export type AsObject = {
}
}

View file

@ -1,290 +0,0 @@
// source: tick.proto
/**
* @fileoverview
* @enhanceable
* @suppress {messageConventions} JS Compiler reports an error if a variable or
* field starts with 'MSG_' and isn't a translatable message.
* @public
*/
// GENERATED CODE -- DO NOT EDIT!
var jspb = require('google-protobuf');
var goog = jspb;
var global = Function('return this')();
goog.exportSymbol('proto.proto.Tick', null, global);
goog.exportSymbol('proto.proto.TickRequest', null, global);
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.proto.Tick = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.proto.Tick, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.proto.Tick.displayName = 'proto.proto.Tick';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.proto.TickRequest = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.proto.TickRequest, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.proto.TickRequest.displayName = 'proto.proto.TickRequest';
}
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.proto.Tick.prototype.toObject = function(opt_includeInstance) {
return proto.proto.Tick.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.proto.Tick} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.proto.Tick.toObject = function(includeInstance, msg) {
var f, obj = {
time: jspb.Message.getFieldWithDefault(msg, 1, 0)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.proto.Tick}
*/
proto.proto.Tick.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.proto.Tick;
return proto.proto.Tick.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.proto.Tick} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.proto.Tick}
*/
proto.proto.Tick.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {number} */ (reader.readInt64());
msg.setTime(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.proto.Tick.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.proto.Tick.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.proto.Tick} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.proto.Tick.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getTime();
if (f !== 0) {
writer.writeInt64(
1,
f
);
}
};
/**
* optional int64 time = 1;
* @return {number}
*/
proto.proto.Tick.prototype.getTime = function() {
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0));
};
/**
* @param {number} value
* @return {!proto.proto.Tick} returns this
*/
proto.proto.Tick.prototype.setTime = function(value) {
return jspb.Message.setProto3IntField(this, 1, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.proto.TickRequest.prototype.toObject = function(opt_includeInstance) {
return proto.proto.TickRequest.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.proto.TickRequest} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.proto.TickRequest.toObject = function(includeInstance, msg) {
var f, obj = {
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.proto.TickRequest}
*/
proto.proto.TickRequest.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.proto.TickRequest;
return proto.proto.TickRequest.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.proto.TickRequest} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.proto.TickRequest}
*/
proto.proto.TickRequest.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.proto.TickRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.proto.TickRequest.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.proto.TickRequest} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.proto.TickRequest.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
};
goog.object.extend(exports, proto.proto);

View file

@ -1,95 +0,0 @@
// package: proto
// file: tick.proto
import * as tick_pb from './tick_pb';
import { grpc } from '@improbable-eng/grpc-web';
type TickServiceSubscribe = {
readonly methodName: string;
readonly service: typeof TickService;
readonly requestStream: false;
readonly responseStream: true;
readonly requestType: typeof tick_pb.TickRequest;
readonly responseType: typeof tick_pb.Tick;
};
type TickServiceNow = {
readonly methodName: string;
readonly service: typeof TickService;
readonly requestStream: false;
readonly responseStream: false;
readonly requestType: typeof tick_pb.TickRequest;
readonly responseType: typeof tick_pb.Tick;
};
export class TickService {
static readonly serviceName: string;
static readonly Subscribe: TickServiceSubscribe;
static readonly Now: TickServiceNow;
}
export type ServiceError = {
message: string;
code: number;
metadata: grpc.Metadata;
};
export type Status = { details: string; code: number; metadata: grpc.Metadata };
interface UnaryResponse {
cancel(): void;
}
interface ResponseStream<T> {
cancel(): void;
on(type: 'data', handler: (message: T) => void): ResponseStream<T>;
on(type: 'end', handler: (status?: Status) => void): ResponseStream<T>;
on(type: 'status', handler: (status: Status) => void): ResponseStream<T>;
}
interface RequestStream<T> {
write(message: T): RequestStream<T>;
end(): void;
cancel(): void;
on(type: 'end', handler: (status?: Status) => void): RequestStream<T>;
on(type: 'status', handler: (status: Status) => void): RequestStream<T>;
}
interface BidirectionalStream<ReqT, ResT> {
write(message: ReqT): BidirectionalStream<ReqT, ResT>;
end(): void;
cancel(): void;
on(
type: 'data',
handler: (message: ResT) => void
): BidirectionalStream<ReqT, ResT>;
on(
type: 'end',
handler: (status?: Status) => void
): BidirectionalStream<ReqT, ResT>;
on(
type: 'status',
handler: (status: Status) => void
): BidirectionalStream<ReqT, ResT>;
}
export class TickServiceClient {
readonly serviceHost: string;
constructor(serviceHost: string, options?: grpc.RpcOptions);
subscribe(
requestMessage: tick_pb.TickRequest,
metadata?: grpc.Metadata
): ResponseStream<tick_pb.Tick>;
now(
requestMessage: tick_pb.TickRequest,
metadata: grpc.Metadata,
callback: (
error: ServiceError | null,
responseMessage: tick_pb.Tick | null
) => void
): UnaryResponse;
now(
requestMessage: tick_pb.TickRequest,
callback: (
error: ServiceError | null,
responseMessage: tick_pb.Tick | null
) => void
): UnaryResponse;
}

View file

@ -1,109 +0,0 @@
// package: proto
// file: tick.proto
var tick_pb = require("./tick_pb");
var grpc = require("@improbable-eng/grpc-web").grpc;
var TickService = (function () {
function TickService() {}
TickService.serviceName = "proto.TickService";
return TickService;
}());
TickService.Subscribe = {
methodName: "Subscribe",
service: TickService,
requestStream: false,
responseStream: true,
requestType: tick_pb.TickRequest,
responseType: tick_pb.Tick
};
TickService.Now = {
methodName: "Now",
service: TickService,
requestStream: false,
responseStream: false,
requestType: tick_pb.TickRequest,
responseType: tick_pb.Tick
};
exports.TickService = TickService;
function TickServiceClient(serviceHost, options) {
this.serviceHost = serviceHost;
this.options = options || {};
}
TickServiceClient.prototype.subscribe = function subscribe(requestMessage, metadata) {
var listeners = {
data: [],
end: [],
status: []
};
var client = grpc.invoke(TickService.Subscribe, {
request: requestMessage,
host: this.serviceHost,
metadata: metadata,
transport: this.options.transport,
debug: this.options.debug,
onMessage: function (responseMessage) {
listeners.data.forEach(function (handler) {
handler(responseMessage);
});
},
onEnd: function (status, statusMessage, trailers) {
listeners.status.forEach(function (handler) {
handler({ code: status, details: statusMessage, metadata: trailers });
});
listeners.end.forEach(function (handler) {
handler({ code: status, details: statusMessage, metadata: trailers });
});
listeners = null;
}
});
return {
on: function (type, handler) {
listeners[type].push(handler);
return this;
},
cancel: function () {
listeners = null;
client.close();
}
};
};
TickServiceClient.prototype.now = function now(requestMessage, metadata, callback) {
if (arguments.length === 2) {
callback = arguments[1];
}
var client = grpc.unary(TickService.Now, {
request: requestMessage,
host: this.serviceHost,
metadata: metadata,
transport: this.options.transport,
debug: this.options.debug,
onEnd: function (response) {
if (callback) {
if (response.status !== grpc.Code.OK) {
var err = new Error(response.statusMessage);
err.code = response.status;
err.metadata = response.trailers;
callback(err, null);
} else {
callback(null, response.message);
}
}
}
});
return {
cancel: function () {
callback = null;
client.close();
}
};
};
exports.TickServiceClient = TickServiceClient;

View file

@ -1,127 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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 'whatwg-fetch';
import localStorage from './localStorage';
import parseError, { ApiError } from './parseError';
const api = {
get(url) {
return api.fetchJson(url);
},
post(url, data) {
return api.fetchJson(url, {
body: JSON.stringify(data),
method: 'POST',
});
},
delete(url, data) {
return api.fetchJson(url, {
body: JSON.stringify(data),
method: 'DELETE',
});
},
put(url, data) {
return api.fetchJson(url, {
body: JSON.stringify(data),
method: 'PUT',
});
},
fetchJson(url, params) {
return new Promise((resolve, reject) => {
this.fetch(url, params)
.then(response => {
if (response.ok) {
return response
.json()
.then(json => resolve(json))
.catch(err => reject(new ApiError(err.message, response)));
} else {
return response
.json()
.then(json => reject(new ApiError(parseError(json), response)))
.catch(() => {
reject(
new ApiError(`${response.status} - ${response.url}`, response)
);
});
}
})
.catch(err => {
reject(err);
});
});
},
fetch(url, params = {}) {
url = window.location.origin + url;
const options = {
...requestOptions,
...params,
};
options.headers = {
...options.headers,
...getAuthHeaders(),
cache: 'no-store',
mode: 'same-origin',
};
// native call
return fetch(url, options);
},
};
const requestOptions = {
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
},
};
export function getAuthHeaders() {
const accessToken = getAccessToken();
const csrfToken = getXCSRFToken();
return {
'X-CSRF-Token': csrfToken,
Authorization: `Bearer ${accessToken}`,
};
}
export function getNoCacheHeaders() {
return {
'cache-control': 'max-age=0',
expires: '0',
pragma: 'no-cache',
};
}
export const getXCSRFToken = () => {
const metaTag = document.querySelector('[name=grv_csrf_token]');
return metaTag ? metaTag.content : '';
};
export function getAccessToken() {
const bearerToken = localStorage.getBearerToken() || {};
return bearerToken.accessToken;
}
export default api;

View file

@ -1,68 +0,0 @@
/*
Copyright 2019 Gravitational, Inc.
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 const KeysEnum = {
TOKEN: 'grv_teleport_token',
TOKEN_RENEW: 'grv_teleport_token_renew'
}
export class BearerToken {
constructor(json){
this.accessToken = json.token;
this.expiresIn = json.expires_in;
this.created = new Date().getTime();
}
}
const storage = {
clear() {
window.localStorage.clear()
},
subscribe(fn){
window.addEventListener('storage', fn);
},
unsubscribe(fn) {
window.removeEventListener('storage', fn);
},
setBearerToken(token) {
window.localStorage.setItem(KeysEnum.TOKEN, JSON.stringify(token));
},
getBearerToken(){
const item = window.localStorage.getItem(KeysEnum.TOKEN);
if (item) {
return JSON.parse(item);
}
return null;
},
getAccessToken(){
const bearerToken = this.getBearerToken();
return bearerToken ? bearerToken.accessToken : null;
},
broadcast(messageType, messageBody){
window.localStorage.setItem(messageType, messageBody);
window.localStorage.removeItem(messageType);
}
}
export default storage;

View file

@ -1,39 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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 function parseError(json) {
let msg = '';
if (json && json.error) {
msg = json.error.message;
} else if (json && json.message) {
msg = json.message;
} else if (json && json.error) {
msg = json.error.message;
} else if (json.responseText) {
msg = json.responseText;
}
return msg;
}
export class ApiError extends Error {
constructor(message, response) {
message = message || 'Unknown error';
super(message);
this.response = response;
this.name = 'ApiError';
}
}

View file

@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json",
}

View file

@ -1,12 +0,0 @@
const defaultCfg = require('@gravitational/build/webpack/webpack.prod.config');
defaultCfg.module.rules.push({
test: /proto\/\.js$/,
loader: 'string-replace-loader',
options: {
search: "var global = Function('return this')();",
replace: 'var global = (function(){ return this }).call(null);',
},
});
module.exports = defaultCfg;

View file

@ -16,37 +16,53 @@ limitations under the License.
import React from 'react';
import styled from 'styled-components';
import { storiesOf } from '@storybook/react';
import BpfViewer, { formatEvents } from './BpfViewer';
import bpfVim from './fixtures/vim';
import bpfTroubleshoot from './fixtures/troubleshoot';
import bpfNpm from './fixtures/npm';
storiesOf('Shared/BpfViewer', module)
.add('vim', () => {
return (
<Box>
<Viewer events={bpfVim.filter(e => e.event === 'session.exec')} />
</Box>
);
})
.add('troubleshoot', () => {
return (
<Box>
<Viewer
events={bpfTroubleshoot.filter(e => e.event !== 'session.exec')}
/>
</Box>
);
})
.add('npm', () => {
return (
<Box>
<Viewer events={bpfNpm.filter(e => e.event === 'session.exec')} />
</Box>
);
export default {
title: 'Shared/BpfViewer',
};
export const Vim = () => {
const events = useMockedEvents(
import('./fixtures/vim').then(vim => vim.default)
);
if (events.length === 0) {
return null;
}
return (
<Box>
<Viewer events={events} />
</Box>
);
};
export const Npm = () => {
const events = useMockedEvents(
import('./fixtures/npm').then(vim => vim.default)
);
if (events.length === 0) {
return null;
}
return (
<Box>
<Viewer events={events} />
</Box>
);
};
function useMockedEvents(loader) {
const [events, setEvents] = React.useState([]);
loader.then(data => {
setEvents(data);
});
return events.filter(e => e.event === 'session.exec');
}
function Viewer({ events, mode = 'tree' }) {
const ref = React.useRef();
React.useEffect(() => {

View file

@ -1,2 +0,0 @@
import events from './bpf.troubleshoot.json';
export default events;

View file

@ -28,6 +28,7 @@ export default function FieldInput({
type = 'text',
autoFocus = false,
autoComplete = 'off',
readonly = false,
...styles
}: Props) {
const { valid, message } = useRule(rule(value));
@ -45,6 +46,7 @@ export default function FieldInput({
autoComplete={autoComplete}
onChange={onChange}
onKeyPress={onKeyPress}
readOnly={readonly}
/>
</Box>
);
@ -62,6 +64,7 @@ type Props = {
rule?: Function;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyPress?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
readonly?: boolean;
// TS: temporary handles ...styles
[key: string]: any;
};

View file

@ -31,6 +31,8 @@ export default function FieldSelect({
rule = defaultRule,
isSearchable = false,
isSimpleValue = false,
autoFocus = false,
isDisabled = false,
...styles
}: Props) {
const { valid, message } = useRule(rule(value));
@ -50,6 +52,8 @@ export default function FieldSelect({
maxMenuHeight={maxMenuHeight}
placeholder={placeholder}
isMulti={isMulti}
autoFocus={autoFocus}
isDisabled={isDisabled}
/>
</Box>
);

View file

@ -164,6 +164,10 @@ exports[`shared/components/Invite.story story.Otp 1`] = `
opacity: 0.4;
}
.c7:read-only {
cursor: not-allowed;
}
.c6 {
color: #FFFFFF;
display: block;
@ -519,6 +523,10 @@ exports[`shared/components/Invite.story story.OtpError 1`] = `
opacity: 0.4;
}
.c8:read-only {
cursor: not-allowed;
}
.c7 {
color: #FFFFFF;
display: block;
@ -805,6 +813,10 @@ exports[`shared/components/Invite.story story.U2f 1`] = `
opacity: 0.4;
}
.c7:read-only {
cursor: not-allowed;
}
.c6 {
color: #FFFFFF;
display: block;
@ -1080,6 +1092,10 @@ exports[`shared/components/Invite.story story.off 1`] = `
opacity: 0.4;
}
.c7:read-only {
cursor: not-allowed;
}
.c6 {
color: #FFFFFF;
display: block;

View file

@ -95,6 +95,10 @@ exports[`auth2faType: otp rendering 1`] = `
opacity: 0.4;
}
.c5:read-only {
cursor: not-allowed;
}
.c4 {
color: #FFFFFF;
display: block;
@ -306,6 +310,10 @@ exports[`auth2faType: u2f rendering 1`] = `
opacity: 0.4;
}
.c5:read-only {
cursor: not-allowed;
}
.c4 {
color: #FFFFFF;
display: block;
@ -803,6 +811,10 @@ exports[`server error rendering 1`] = `
opacity: 0.4;
}
.c6:read-only {
cursor: not-allowed;
}
.c5 {
color: #FFFFFF;
display: block;

View file

@ -25,12 +25,12 @@ export default function Select(props: Props) {
return (
<StyledSelect hasError={hasError}>
<ReactSelect
menuPlacement="auto"
className="react-select-container"
classNamePrefix="react-select"
clearable={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
placeholder="Select..."
{...restOfProps}
/>
@ -47,7 +47,6 @@ export function SelectAsync(props: AsyncProps) {
classNamePrefix="react-select"
clearable={false}
isSearchable={true}
maxMenuHeight={300}
defaultOptions={false}
cacheOptions={false}
defaultMenuIsOpen={false}

View file

@ -19,15 +19,19 @@ export type Props = {
clearable?: boolean;
isSimpleValue?: boolean;
isSearchable?: boolean;
isDisabled?: boolean;
maxMenuHeight?: number;
onChange(e: Option): void;
value: null | Option;
onChange(e: Option | Option[]): void;
value: null | Option | Option[];
isMulti?: boolean;
autoFocus?: boolean;
label?: string;
placeholder?: string;
options: Option[];
width?: string | number;
menuPlacement?: string;
components?: any;
menuPosition?: 'fixed' | 'absolute';
};
export type AsyncProps = Omit<Props, 'options'> & {

View file

@ -14,10 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* requiredField checks for empty strings and arrays.
*
* @param message The custom error message to display to users.
* @param value The value user entered.
*/
const requiredField = message => value => () => {
const valid = !(!value || value.length === 0);
return {
valid: !!value,
message: !value ? message : '',
valid,
message: !valid ? message : '',
};
};

View file

@ -19,10 +19,12 @@ import withState from './withState';
import useAttempt from './useAttempt';
import useFavicon from './useFavicon';
import useDocTitle from './useDocTitle';
import useAttemptNext from './useAttemptNext';
export {
useRef,
useAttempt,
useAttemptNext,
useState,
withState,
useEffect,

View file

@ -0,0 +1,56 @@
/*
Copyright 2020 Gravitational, Inc.
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 from 'react';
import Logger from 'shared/libs/logger';
const logger = Logger.create('shared/hooks/useAttempt');
// This is the next version of existing useAttempt hook
export default function useAttemptNext(status = '' as Attempt['status']) {
const [attempt, setAttempt] = React.useState<Attempt>(() => ({
status,
statusText: '',
}));
function handleError(err: Error) {
logger.error('attempt', err);
setAttempt({ status: 'failed', statusText: err.message });
}
function run(fn: Callback) {
try {
setAttempt({ status: 'processing' });
return fn()
.then(() => {
setAttempt({ status: 'success' });
})
.catch(err => {
handleError(err);
});
} catch (err) {
handleError(err);
}
}
return { attempt, setAttempt, run };
}
export type Attempt = {
status: 'processing' | 'failed' | 'success' | '';
statusText?: string;
};
type Callback = (fn?: any) => Promise<any>;

View file

@ -1,72 +0,0 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 FeatureBase, { Activator } from './featureBase';
jest.mock('./logger', () => {
const mockLogger = {
error: jest.fn(),
};
return {
create: () => mockLogger,
};
});
test('class FeatureBase: setters and boolean getters', () => {
const fb = new FeatureBase();
// default states
expect(fb.state.statusText).toBe('');
expect(fb.state.status).toBe('uninitialized');
expect(fb.isProcessing()).toBe(false);
expect(fb.isReady()).toBe(false);
expect(fb.isDisabled()).toBe(false);
expect(fb.isFailed()).toBe(false);
fb.setFailed(new Error('errMsg'));
expect(fb.state.statusText).toBe('errMsg');
expect(fb.isFailed()).toBe(true);
fb.setProcessing();
expect(fb.isProcessing()).toBe(true);
fb.setReady();
expect(fb.isReady()).toBe(true);
fb.setDisabled();
expect(fb.isDisabled()).toBe(true);
});
test('class Activator', () => {
const loadable1 = {
onload: jest.fn(),
};
const loadable2 = {
onload: jest.fn(),
};
const ctx = {
name: 'sam',
};
const activator = new Activator<any>([loadable1, loadable2]);
activator.onload(ctx);
expect(loadable1.onload).toHaveBeenCalledWith(ctx);
expect(loadable1.onload).toHaveBeenCalledTimes(1);
expect(loadable2.onload).toHaveBeenCalledTimes(1);
});

View file

@ -1,97 +0,0 @@
/*
Copyright 2019 Gravitational, Inc.
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 { Store } from './stores';
import Logger from 'shared/libs/logger';
const logger = Logger.create('featureBase');
type Status = 'ready' | 'processing' | 'failed' | 'uninitialized' | 'disabled';
const defaultState = {
status: 'uninitialized' as Status,
statusText: '',
};
export default class FeatureBase extends Store<typeof defaultState> {
state = {
...defaultState,
};
setProcessing() {
this._setStatus('processing');
}
setReady() {
this._setStatus('ready');
}
setDisabled() {
this._setStatus('disabled');
}
setFailed(err: Error) {
logger.error(err);
this._setStatus('failed', err.message);
}
isReady() {
return this.state.status === 'ready';
}
isProcessing() {
return this.state.status === 'processing';
}
isFailed() {
return this.state.status === 'failed';
}
isDisabled() {
return this.state.status === 'disabled';
}
_setStatus(status: Status, statusText = '') {
this.setState({ status, statusText });
}
}
// Activator invokes onload method on a group of features.
export class Activator<T> {
features: Loadable[];
constructor(features: Loadable[]) {
this.features = features || [];
}
onload(ctx: T) {
this.features.forEach(f => {
this._invokeOnload(f, ctx);
});
}
_invokeOnload(f, ...props) {
try {
f.onload(...props);
} catch (err) {
logger.error('failed to invoke onload()', err);
}
}
}
type Loadable = {
onload(...params: any[]): void;
};

View file

@ -16,7 +16,7 @@
import React from 'react';
import { AccessStrategy } from './AccessStrategy';
import { RequestPending } from './RequestPending';
import RequestPending from './RequestPending';
export default {
title: 'Teleport/AccessStrategy',

View file

@ -0,0 +1,228 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import AccessStrategy from './AccessStrategy';
import userService, {
makeUserContext,
makeAccessRequest,
} from 'teleport/services/user';
import { render, screen, wait, fireEvent } from 'design/utils/testing';
import localStorage from 'teleport/services/localStorage';
import historyService from 'teleport/services/history';
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'error').mockImplementation();
});
test('strategy "optional"', async () => {
const userContext = makeUserContext(sampleContext('optional'));
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
render(<AccessStrategy children={<>hello</>} />);
await wait(() => expect(screen.getByText(/hello/i)).toBeInTheDocument());
});
test('strategy "reason" dialog', async () => {
const userContext = makeUserContext(sampleContext());
userContext.accessStrategy.type = 'reason';
userContext.accessStrategy.prompt = 'custom prompt';
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
render(<AccessStrategy />);
await wait(() =>
expect(screen.getByText(/custom prompt/i)).toBeInTheDocument()
);
});
test('strategy "reason" submit action', async () => {
const request = makeAccessRequest({ ...sampleRequest, state: 'PENDING' });
const userContext = makeUserContext(sampleContext('reason'));
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
jest.spyOn(userService, 'createAccessRequest').mockResolvedValue(request);
jest.spyOn(userService, 'fetchAccessRequest').mockResolvedValue(request);
render(<AccessStrategy />);
await wait(() =>
expect(screen.getByText(/send request/i)).toBeInTheDocument()
);
fireEvent.change(screen.getByPlaceholderText(/describe/i), {
target: { value: 'reason' },
});
await wait(() => fireEvent.click(screen.getByText(/send request/i)));
expect(screen.getByText(/being authorized/i)).toBeInTheDocument();
expect(userService.createAccessRequest).toHaveBeenCalledTimes(1);
expect(userService.fetchAccessRequest).toHaveBeenCalled();
});
test('strategy "reason" submit action error', async () => {
const userContext = makeUserContext(sampleContext('reason'));
const err = new Error('some error');
jest.spyOn(localStorage, 'getAccessRequestResult').mockReturnValue(null);
jest.spyOn(userService, 'createAccessRequest').mockRejectedValue(err);
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
render(<AccessStrategy />);
await wait(() =>
expect(screen.getByText(/send request/i)).toBeInTheDocument()
);
fireEvent.change(screen.getByPlaceholderText(/describe/i), {
target: { value: 'reason' },
});
await wait(() => fireEvent.click(screen.getByText(/send request/i)));
expect(screen.getByText(/send request/i)).toBeInTheDocument();
expect(screen.getByText(/some error/i)).toBeInTheDocument();
});
test('strategy "always" renders pending dialog, with request state empty', async () => {
const request = makeAccessRequest({ ...sampleRequest, state: 'PENDING' });
const userContext = makeUserContext(sampleContext('always'));
jest.spyOn(localStorage, 'setAccessRequestResult').mockImplementation();
jest.spyOn(userService, 'createAccessRequest').mockResolvedValue(request);
jest.spyOn(userService, 'fetchAccessRequest').mockResolvedValue(request);
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
render(<AccessStrategy checkerInterval={0} />);
await wait(() => {
expect(screen.getByText(/being authorized/i)).toBeInTheDocument();
});
// When access request state is initially empty,
// hook should auto create request before fetching request.
expect(userService.createAccessRequest).toHaveBeenCalledTimes(1);
expect(userService.fetchAccessRequest).toHaveBeenCalled();
expect(localStorage.setAccessRequestResult).toHaveBeenCalledWith(request);
});
test('strategy "always" renders pending dialog, with request state PENDING', async () => {
const request = makeAccessRequest({ ...sampleRequest, state: 'PENDING' });
const userContext = makeUserContext(sampleContext('always'));
jest
.spyOn(localStorage, 'getAccessRequestResult')
.mockReturnValueOnce(request);
jest.spyOn(userService, 'createAccessRequest').mockResolvedValue(request);
jest.spyOn(userService, 'fetchAccessRequest').mockResolvedValue(request);
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
await wait(() => render(<AccessStrategy checkerInterval={0} />));
expect(screen.getByText(/being authorized/i)).toBeInTheDocument();
expect(userService.createAccessRequest).not.toHaveBeenCalled();
expect(userService.fetchAccessRequest).toHaveBeenCalled();
});
test('strategy "always" with request APPROVED', async () => {
const request = makeAccessRequest({ ...sampleRequest, state: 'APPROVED' });
const userContext = makeUserContext(sampleContext('always'));
jest.spyOn(localStorage, 'setAccessRequestResult').mockImplementation();
jest.spyOn(localStorage, 'getAccessRequestResult').mockReturnValue(request);
jest.spyOn(userService, 'fetchAccessRequest').mockResolvedValue(request);
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
jest.spyOn(userService, 'applyPermission').mockResolvedValue({});
jest.spyOn(historyService, 'reload').mockImplementation();
render(<AccessStrategy checkerInterval={0} children={<>hello</>} />);
await wait(() =>
expect(userService.applyPermission).toHaveBeenCalledTimes(1)
);
// Fetching access request happens with pending,
// so this proves pending dialog was briefly rendered.
expect(userService.fetchAccessRequest).toHaveBeenCalled();
expect(historyService.reload).toHaveBeenCalledTimes(1);
expect(localStorage.setAccessRequestResult).toHaveBeenCalledWith({
...sampleRequest,
state: 'APPLIED',
});
});
test('strategy "always" with request DENIED', async () => {
const request = makeAccessRequest({ ...sampleRequest, state: 'DENIED' });
const userContext = makeUserContext(sampleContext('always'));
jest.spyOn(localStorage, 'getAccessRequestResult').mockReturnValue(request);
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
render(<AccessStrategy />);
await wait(() =>
expect(screen.getByText(/request denied/i)).toBeInTheDocument()
);
});
test('strategy "always" with request APPLIED', async () => {
const request = makeAccessRequest({ ...sampleRequest, state: 'APPLIED' });
const userContext = makeUserContext(sampleContext('always'));
jest.spyOn(localStorage, 'getAccessRequestResult').mockReturnValue(request);
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
render(<AccessStrategy children={<>hello</>} />);
await wait(() => expect(screen.getByText(/hello/i)).toBeInTheDocument());
});
test('strategy "always" fetch request errors', async () => {
const request = makeAccessRequest({ ...sampleRequest, state: 'APPROVED' });
const userContext = makeUserContext(sampleContext('always'));
jest.spyOn(localStorage, 'getAccessRequestResult').mockReturnValue(request);
jest
.spyOn(userService, 'fetchAccessRequest')
.mockRejectedValue(new Error('some error'));
jest.spyOn(userService, 'fetchUserContext').mockResolvedValue(userContext);
render(<AccessStrategy checkerInterval={0} />);
await wait(() =>
expect(screen.getByText(/error has occurred/i)).toBeInTheDocument()
);
});
const sampleRequest = {
id: '',
state: '',
reason: '',
};
const sampleContext = (type = '') => ({
accessStrategy: {
type,
prompt: '',
},
cluster: {
name: 'im-a-cluster-name',
lastConnected: '2020-11-04T19:07:50.693Z',
connectedText: '2020-11-04 11:07:50',
status: 'online',
url: '/web/cluster/im-a-cluster-name',
authVersion: '5.0.0-dev',
nodeCount: 1,
publicURL: 'localhost:3080',
proxyVersion: '5.0.0-dev',
},
});

View file

@ -15,7 +15,6 @@
*/
import React from 'react';
import styled from 'styled-components';
import { Indicator } from 'design';
import { AppVerticalSplit } from 'teleport/components/Layout';
import AjaxPoller from 'teleport/components/AjaxPoller';
@ -25,12 +24,14 @@ import RequestDenied from './RequestDenied';
import RequestError from './RequestError';
import useAccessStrategy, { State } from './useAccessStrategy';
export default function Container(props: Props) {
const Container: React.FC<Props> = props => {
const state = useAccessStrategy();
return <AccessStrategy {...props} {...state} />;
}
};
export function AccessStrategy(props: State & Props) {
export default Container;
export const AccessStrategy: React.FC<State & Props> = props => {
const {
children,
attempt,
@ -43,9 +44,11 @@ export function AccessStrategy(props: State & Props) {
if (attempt.isProcessing) {
return (
<StyledIndicator>
<AppVerticalSplit
style={{ alignItems: 'center', justifyContent: 'center' }}
>
<Indicator />
</StyledIndicator>
</AppVerticalSplit>
);
}
@ -83,14 +86,8 @@ export function AccessStrategy(props: State & Props) {
}
return null;
}
const StyledIndicator = styled(AppVerticalSplit)`
align-items: center;
justify-content: center;
`;
};
type Props = {
children: React.ReactNode;
checkerInterval?: number;
};

View file

@ -39,7 +39,7 @@ export default function RequestError({ err }: Props) {
</DialogContent>
<DialogFooter>
<ButtonSecondary onClick={() => session.logout()}>
Cancel & Logout
{`Cancel & Logout`}
</ButtonSecondary>
</DialogFooter>
</Dialog>

View file

@ -0,0 +1,19 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 RequestError from './RequestError';
export default RequestError;

View file

@ -21,11 +21,7 @@ import { Text, ButtonLink, Flex } from 'design';
import { ShieldCheck } from 'design/Icon';
import Dialog, { DialogContent, DialogFooter } from 'design/Dialog';
export default function Container(props) {
return <RequestPending {...props} />;
}
export function RequestPending() {
export default function RequestPending() {
return (
<Dialog
dialogCss={() => ({
@ -38,10 +34,10 @@ export function RequestPending() {
<DialogContent alignItems="center">
<ShieldCheck fontSize={40} mb={3} />
<Text mb={1} bold caps>
Your account is being authorized
Your access is being authorized
</Text>
<Text mb={4}>
Please wait while an administrator authorizes your account.
Please wait while an administrator authorizes your access.
</Text>
<StyledProgressBar height="16px" value={100} mb={4}>
<Bar />

View file

@ -0,0 +1,19 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 RequestPending from './RequestPending';
export default RequestPending;

View file

@ -14,25 +14,17 @@
* limitations under the License.
*/
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import session from 'teleport/services/session';
import { ButtonPrimary, ButtonSecondary, Text, Alert, Box } from 'design';
import {
ButtonPrimary,
ButtonSecondary,
Text,
LabelInput,
Alert,
Box,
} from 'design';
import Dialog, {
DialogHeader,
DialogTitle,
@ -59,19 +51,17 @@ export function RequestReason(props: ReturnType<typeof useRequestReason>) {
open={true}
>
<DialogHeader>
<DialogTitle>Request Account Access</DialogTitle>
<DialogTitle>Request Access</DialogTitle>
</DialogHeader>
<DialogContent>
{attempt.isFailed && <Alert kind="danger" children={attempt.message} />}
<Text mb={4}>{requestPrompt}</Text>
<Text typography="subtitle2" caps mb={1}>
Authorization Request Message
</Text>
<LabelInput mb={1}>Authorization Request Message</LabelInput>
<Box
height="100px"
as="textarea"
p={2}
borderRadius={1}
borderRadius={2}
placeholder="Describe your request..."
value={reason}
onChange={e => setReason(e.target.value)}

View file

@ -26,7 +26,7 @@ exports[`failed 1`] = `
box-sizing: border-box;
padding: 8px;
height: 100px;
border-radius: 2px;
border-radius: 4px;
}
.c12 {
@ -114,6 +114,16 @@ exports[`failed 1`] = `
color: rgba(255,255,255,0.3);
}
.c9 {
color: #FFFFFF;
display: block;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
width: 100%;
margin-bottom: 4px;
}
.c5 {
overflow: hidden;
text-overflow: ellipsis;
@ -132,17 +142,6 @@ exports[`failed 1`] = `
margin-bottom: 24px;
}
.c9 {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
font-size: 10px;
line-height: 16px;
text-transform: uppercase;
margin: 0px;
margin-bottom: 4px;
}
.c1 {
z-index: -1;
position: fixed;
@ -235,7 +234,7 @@ exports[`failed 1`] = `
class="c5"
color="text.primary"
>
Request Account Access
Request Access
</div>
</div>
<div
@ -252,11 +251,12 @@ exports[`failed 1`] = `
>
To access your Teleport account, please send an authorization request using the form below.
</div>
<div
<label
class="c9"
font-size="0"
>
Authorization Request Message
</div>
</label>
<textarea
class="c10"
height="100px"
@ -292,7 +292,7 @@ exports[`loaded with custom prompt 1`] = `
box-sizing: border-box;
padding: 8px;
height: 100px;
border-radius: 2px;
border-radius: 4px;
}
.c11 {
@ -380,6 +380,16 @@ exports[`loaded with custom prompt 1`] = `
color: rgba(255,255,255,0.3);
}
.c8 {
color: #FFFFFF;
display: block;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
width: 100%;
margin-bottom: 4px;
}
.c5 {
overflow: hidden;
text-overflow: ellipsis;
@ -398,17 +408,6 @@ exports[`loaded with custom prompt 1`] = `
margin-bottom: 24px;
}
.c8 {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
font-size: 10px;
line-height: 16px;
text-transform: uppercase;
margin: 0px;
margin-bottom: 4px;
}
.c1 {
z-index: -1;
position: fixed;
@ -501,7 +500,7 @@ exports[`loaded with custom prompt 1`] = `
class="c5"
color="text.primary"
>
Request Account Access
Request Access
</div>
</div>
<div
@ -512,11 +511,12 @@ exports[`loaded with custom prompt 1`] = `
>
Some custom prompt set by administrator
</div>
<div
<label
class="c8"
font-size="0"
>
Authorization Request Message
</div>
</label>
<textarea
class="c9"
height="100px"
@ -552,7 +552,7 @@ exports[`loaded without custom prompt 1`] = `
box-sizing: border-box;
padding: 8px;
height: 100px;
border-radius: 2px;
border-radius: 4px;
}
.c11 {
@ -640,6 +640,16 @@ exports[`loaded without custom prompt 1`] = `
color: rgba(255,255,255,0.3);
}
.c8 {
color: #FFFFFF;
display: block;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
width: 100%;
margin-bottom: 4px;
}
.c5 {
overflow: hidden;
text-overflow: ellipsis;
@ -658,17 +668,6 @@ exports[`loaded without custom prompt 1`] = `
margin-bottom: 24px;
}
.c8 {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
font-size: 10px;
line-height: 16px;
text-transform: uppercase;
margin: 0px;
margin-bottom: 4px;
}
.c1 {
z-index: -1;
position: fixed;
@ -761,7 +760,7 @@ exports[`loaded without custom prompt 1`] = `
class="c5"
color="text.primary"
>
Request Account Access
Request Access
</div>
</div>
<div
@ -772,11 +771,12 @@ exports[`loaded without custom prompt 1`] = `
>
To access your Teleport account, please send an authorization request using the form below.
</div>
<div
<label
class="c8"
font-size="0"
>
Authorization Request Message
</div>
</label>
<textarea
class="c9"
height="100px"

View file

@ -15,9 +15,9 @@
*/
import React from 'react';
import cfg from 'teleport/config';
import sessionStorage from 'teleport/services/localStorage';
import useAttempt from 'shared/hooks/useAttempt';
import historyService from 'teleport/services/history';
import userService, {
AccessStrategy,
AccessRequest,
@ -25,17 +25,15 @@ import userService, {
} from 'teleport/services/user';
export default function useAccessStrategy() {
const clusterId = cfg.proxyCluster; // root cluster
const [attempt, attemptActions] = useAttempt({ isProcessing: true });
const [strategy, setStrategy] = React.useState<AccessStrategy>(null);
const [accessRequest, setAccessRequest] = React.useState<AccessRequest>(
makeAccessRequest(sessionStorage.getAccessRequestResult())
);
React.useEffect(() => {
attemptActions.do(() =>
userService.fetchUser(clusterId).then(res => {
userService.fetchUserContext().then(res => {
setStrategy(res.accessStrategy);
if (
accessRequest.state === '' &&
@ -60,12 +58,11 @@ export default function useAccessStrategy() {
function updateState(result: AccessRequest) {
sessionStorage.setAccessRequestResult(result);
if (result.state === 'APPROVED') {
return userService.applyPermission(result.id).then(() => {
result.state = 'APPLIED';
sessionStorage.setAccessRequestResult(result);
window.location.reload();
historyService.reload();
});
}

View file

@ -17,36 +17,32 @@ limitations under the License.
import React from 'react';
import cfg from 'teleport/config';
import PasswordForm from 'shared/components/FormPassword';
import {
FeatureBox,
FeatureHeader,
FeatureHeaderTitle,
} from 'teleport/components/Layout';
import { FeatureBox } from 'teleport/components/Layout';
import authService from 'teleport/services/auth';
import { Auth2faType } from 'shared/services';
export function Account(props) {
const { auth2faType, onChangePass, onChangePassWithU2f } = props;
export default function Container() {
const state = useAccount();
return <Account {...state} />;
}
export function Account(props: ReturnType<typeof useAccount>) {
const { auth2faType, changePass, changePassWithU2f } = props;
return (
<FeatureBox>
<FeatureHeader>
<FeatureHeaderTitle>Account Settings</FeatureHeaderTitle>
</FeatureHeader>
<FeatureBox pt="4">
<PasswordForm
auth2faType={auth2faType}
onChangePass={onChangePass}
onChangePassWithU2f={onChangePassWithU2f}
onChangePass={changePass}
onChangePassWithU2f={changePassWithU2f}
/>
</FeatureBox>
);
}
export default function(props) {
const settProps = {
...props,
auth2faType: cfg.getAuth2faType(),
onChangePass: authService.changePassword.bind(authService),
onChangePassWithU2f: authService.changePasswordWithU2f.bind(authService),
function useAccount() {
return {
auth2faType: cfg.getAuth2faType() as Auth2faType,
changePass: authService.changePassword.bind(authService),
changePassWithU2f: authService.changePasswordWithU2f.bind(authService),
};
return <Account {...settProps} />;
}

View file

@ -13,23 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import ClusterDialog from './ClusterInfoDialog';
import { AppLauncher } from './AppLauncher';
export default {
title: 'Teleport/ClusterInfoDialog',
title: 'Teleport/AppLauncher',
};
export const ClusterInfoDialog = () => (
<ClusterDialog
onClose={null}
clusterId="applePie"
publicURL="some.kind.of.host:8080"
proxyVersion="4.2.2"
authVersion="5.0.0"
/>
);
ClusterInfoDialog.story = {
name: 'ClusterInfoDialog',
export const Processing = () => {
return <AppLauncher status="processing" statusText="" />;
};
export const Failed = () => {
return <AppLauncher status="failed" statusText="" />;
};

View file

@ -0,0 +1,37 @@
/*
Copyright 2020 Gravitational, Inc.
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 from 'react';
import { Flex, Indicator } from 'design';
import { AccessDenied } from 'design/CardError';
import useAppLauncher from './useAppLauncher';
export default function Container() {
const state = useAppLauncher();
return <AppLauncher {...state} />;
}
export function AppLauncher(props: ReturnType<typeof useAppLauncher>) {
if (props.status === 'failed') {
return <AccessDenied message={props.statusText} />;
}
return (
<Flex height="180px" justifyContent="center" alignItems="center" flex="1">
<Indicator />
</Flex>
);
}

View file

@ -0,0 +1,18 @@
/*
Copyright 2020 Gravitational, Inc.
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 AppLauncher from './AppLauncher';
export default AppLauncher;

View file

@ -0,0 +1,49 @@
/*
Copyright 2020 Gravitational, Inc.
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 from 'react';
import { useParams } from 'react-router';
import service from 'teleport/services/apps';
import useAttempt from 'shared/hooks/useAttemptNext';
import { UrlLauncherParams } from 'teleport/config';
export default function useAppLauncher() {
const params = useParams<UrlLauncherParams>();
const { attempt, setAttempt } = useAttempt('processing');
React.useEffect(() => {
service
.createAppSession(params)
.then(result => {
// make a redirect to the requested app auth endpoint
const location = window.location;
const port = location.port ? ':' + location.port : '';
window.location.replace(
`https://${result.fqdn}${port}/x-teleport-auth#value=${result.value}`
);
})
.catch((err: Error) => {
setAttempt({
status: 'failed',
statusText: err.message,
});
});
}, []);
return {
...attempt,
};
}

View file

@ -0,0 +1,29 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import * as stories from './AddApp.story';
import { render } from 'design/utils/testing';
test('loaded', async () => {
const { getByTestId } = render(<stories.Loaded />);
expect(getByTestId('Modal')).toMatchSnapshot();
});
test('created', async () => {
const { getByTestId } = render(<stories.Created />);
expect(getByTestId('Modal')).toMatchSnapshot();
});

View file

@ -0,0 +1,72 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import { AddApp } from './AddApp';
export default {
title: 'Teleport/Apps/Add',
};
export const Created = () => (
<AddApp {...sample.props} attempt={{ status: 'success' }} />
);
export const Loaded = () => {
return <AddApp {...sample.props} cmd="" />;
};
export const Processing = () => (
<AddApp {...sample.props} attempt={{ status: 'processing' }} />
);
export const Failed = () => (
<AddApp
{...sample.props}
attempt={{ status: 'failed', statusText: 'some error message' }}
/>
);
export const Manually = () => (
<AddApp
{...sample.props}
automatic={false}
attempt={{ status: 'failed', statusText: 'some error message' }}
/>
);
const sample = {
props: {
automatic: true,
setAutomatic: () => null,
createToken: () => Promise.resolve(),
onClose() {
return null;
},
createJoinToken() {
return Promise.resolve(null);
},
version: '5.0.0-dev',
cmd: `sudo bash -c "$(curl -fsSL 'http://localhost/scripts/86/install-app.sh?name=test&uri=http://myapp/')"`,
canCreateToken: true,
expires: '1 hour',
reset: () => null,
attempt: {
status: '',
statusText: '',
} as any,
},
};

View file

@ -0,0 +1,87 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import { Flex } from 'design';
//import * as Icons from 'design/Icon';
import Dialog, { DialogTitle } from 'design/Dialog';
import useTeleport from 'teleport/useTeleport';
//import { TabIcon } from 'teleport/components/Tabs';
import Manually from './Manually';
import Automatically from './Automatically';
import useAddApp, { State } from './useAddApp';
export default function Container(props: Props) {
const ctx = useTeleport();
const state = useAddApp(ctx);
return <AddApp {...state} {...props} />;
}
export function AddApp({
cmd,
expires,
onClose,
createToken,
version,
attempt,
automatic,
}: //setAutomatic,
State & Props) {
return (
<Dialog
dialogCss={() => ({
maxWidth: '600px',
width: '100%',
minHeight: '330px',
})}
disableEscapeKeyDown={false}
onClose={onClose}
open={true}
>
<Flex flex="1" flexDirection="column">
<Flex alignItems="center" justifyContent="space-between" mb="4">
<DialogTitle mr="auto">Add Application</DialogTitle>
{/* <TabIcon
Icon={Icons.Wand}
title="Automatically"
active={automatic}
onClick={() => setAutomatic(true)}
/>
<TabIcon
Icon={Icons.Cog}
title="Manually"
active={!automatic}
onClick={() => setAutomatic(false)}
/> */}
</Flex>
{automatic && (
<Automatically
cmd={cmd}
expires={expires}
onClose={onClose}
onCreate={createToken}
attempt={attempt}
/>
)}
{!automatic && <Manually onClose={onClose} version={version} />}
</Flex>
</Dialog>
);
}
type Props = {
onClose(): void;
};

View file

@ -0,0 +1,188 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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, { KeyboardEvent } from 'react';
import TextSelectCopy from 'teleport/components/TextSelectCopy';
import { Text, Flex, Alert, ButtonSecondary, ButtonPrimary } from 'design';
import Validation, { Validator } from 'shared/components/Validation';
import FieldInput from 'shared/components/FieldInput';
import { DialogContent, DialogFooter } from 'design/Dialog';
import { Attempt } from 'shared/hooks/useAttemptNext';
export default function Automatically(props: Props) {
const { cmd, onClose, attempt, expires } = props;
const [name, setName] = React.useState('');
const [uri, setUri] = React.useState('');
function handleCreate(validator: Validator) {
if (!validator.validate()) {
return;
}
props.onCreate(name, uri);
}
function handleEnterPress(
e: KeyboardEvent<HTMLInputElement>,
validator: Validator
) {
if (e.key === 'Enter') {
handleCreate(validator);
}
}
return (
<Validation>
{({ validator }) => (
<>
<DialogContent minHeight="254px" flex="0 0 auto">
<Flex alignItems="center" flexDirection="row">
<FieldInput
rule={requiredAppName}
label="App Name"
autoFocus
value={name}
placeholder="jenkins"
width="320px"
mr="3"
onKeyPress={e => handleEnterPress(e, validator)}
onChange={e => setName(e.target.value)}
/>
<FieldInput
rule={requiredAppUri}
label="INTERNAL APPLICATION URL"
width="100%"
value={uri}
placeholder="https://localhost:4000"
onKeyPress={e => handleEnterPress(e, validator)}
onChange={e => setUri(e.target.value)}
/>
</Flex>
{!cmd && (
<Text mb="3">
Provide the name and URL of your application to generate a
script that will automatically install and configure the App
service on the server that can access your application.
</Text>
)}
{attempt.status === 'failed' && (
<Alert kind="danger" children={attempt.statusText} />
)}
{cmd && (
<>
<Text mb="3">
Use the script below to add an application to your cluster.{' '}
<br />
The script will be valid for
<Text bold as="span">
{` ${expires}`}.
</Text>
</Text>
<TextSelectCopy text={cmd} mb={2} />
</>
)}
</DialogContent>
<DialogFooter>
{!cmd && (
<ButtonPrimary
mr="3"
disabled={attempt.status === 'processing'}
onClick={() => handleCreate(validator)}
>
Generate Script
</ButtonPrimary>
)}
{cmd && (
<ButtonPrimary
mr="3"
disabled={attempt.status === 'processing'}
onClick={() => handleCreate(validator)}
>
Regenerate
</ButtonPrimary>
)}
<ButtonSecondary
disabled={attempt.status === 'processing'}
onClick={onClose}
>
Close
</ButtonSecondary>
</DialogFooter>
</>
)}
</Validation>
);
}
const requiredAppUri = value => () => {
if (!value || value.length === 0) {
return {
valid: false,
message: 'Required',
};
}
try {
new URL(value);
} catch {
return {
valid: false,
message: 'URL is invalid',
};
}
return {
valid: true,
};
};
const requiredAppName = value => () => {
if (!value || value.length === 0) {
return {
valid: false,
message: 'Required',
};
}
try {
const tmp = new URL(`https://${value}`);
if (tmp.hostname !== value) {
throw new Error();
}
// cannot be a sub-domain
if (tmp.hostname.split('.').length > 1) {
throw new Error();
}
} catch {
return {
valid: false,
message: 'Invalid',
};
}
return {
valid: true,
};
};
type Props = {
onClose(): void;
onCreate(name: string, uri: string): Promise<any>;
cmd: string;
expires: string;
attempt: Attempt;
};

View file

@ -0,0 +1,19 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 Automatically from './Automatically';
export default Automatically;

View file

@ -0,0 +1,85 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import { Text, Box, ButtonSecondary, Link } from 'design';
import { DialogContent, DialogFooter } from 'design/Dialog';
import TextSelectCopy from 'teleport/components/TextSelectCopy';
import cfg from 'teleport/config';
import * as links from 'teleport/services/links';
export default function Manually({ version, onClose }: Props) {
return (
<>
<DialogContent minHeight="240px" flex="0 0 auto">
<Box>
<Box mb={4}>
<Text bold as="span">
Step 1
</Text>{' '}
- Download Teleport package to your computer
<Box>
<Link href={links.getMacOS(version)} target="_blank" mr="2">
MacOS
</Link>
<Link href={links.getLinux64(version)} target="_blank" mr="2">
Linux 64-bit
</Link>
<Link href={links.getLinux32(version)} target="_blank">
Linux 32-bit
</Link>
</Box>
</Box>
<Box mb={4}>
<Text bold as="span">
Step 2
</Text>
{' - Login to Teleport'}
<TextSelectCopy
mt="2"
text={`$ tsh login --proxy=${cfg.proxyCluster} --auth=local`}
/>
</Box>
<Box mb={4}>
<Text bold as="span">
Step 3
</Text>
{' - Generate a join token'}
<TextSelectCopy mt="2" text="$ tctl tokens add --type=app" />
</Box>
<Box>
<Text bold as="span">
Step 4
</Text>
{` - Install Teleport on target server, and start it with the following parameters`}
<TextSelectCopy
mt="2"
text={`$ teleport start --roles=node --token=<generated-node-join-token> --auth-server=${cfg.proxyCluster} `}
/>
</Box>
</Box>
</DialogContent>
<DialogFooter>
<ButtonSecondary onClick={onClose}>Close</ButtonSecondary>
</DialogFooter>
</>
);
}
type Props = {
onClose(): void;
version: string;
};

View file

@ -0,0 +1,19 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 Manually from './Manually';
export default Manually;

View file

@ -0,0 +1,784 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`created 1`] = `
.c9 {
box-sizing: border-box;
margin-bottom: 24px;
margin-right: 16px;
width: 320px;
}
.c12 {
box-sizing: border-box;
margin-bottom: 24px;
width: 100%;
}
.c17 {
line-height: 1.5;
margin: 0;
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
outline: none;
position: relative;
text-align: center;
text-decoration: none;
text-transform: uppercase;
transition: all 0.3s;
-webkit-font-smoothing: antialiased;
background: #512FC9;
color: rgba(255,255,255,0.87);
min-height: 32px;
font-size: 12px;
padding: 0px 24px;
}
.c17:active {
opacity: 0.56;
}
.c17:hover,
.c17:focus {
background: #651FFF;
}
.c17:active {
background: #354AA4;
}
.c17:disabled {
background: rgba(255,255,255,0.12);
color: rgba(255,255,255,0.3);
}
.c19 {
line-height: 1.5;
margin: 0;
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
outline: none;
position: relative;
text-align: center;
text-decoration: none;
text-transform: uppercase;
transition: all 0.3s;
-webkit-font-smoothing: antialiased;
background: #512FC9;
color: rgba(255,255,255,0.87);
min-height: 32px;
font-size: 12px;
padding: 0px 24px;
margin-right: 16px;
}
.c19:active {
opacity: 0.56;
}
.c19:hover,
.c19:focus {
background: #651FFF;
}
.c19:active {
background: #354AA4;
}
.c19:disabled {
background: rgba(255,255,255,0.12);
color: rgba(255,255,255,0.3);
}
.c20 {
line-height: 1.5;
margin: 0;
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
outline: none;
position: relative;
text-align: center;
text-decoration: none;
text-transform: uppercase;
transition: all 0.3s;
-webkit-font-smoothing: antialiased;
background: #222C59;
color: rgba(255,255,255,0.87);
min-height: 32px;
font-size: 12px;
padding: 0px 24px;
}
.c20:active {
opacity: 0.56;
}
.c20:hover,
.c20:focus {
background: #2C3A73;
}
.c20:disabled {
background: rgba(255,255,255,0.12);
color: rgba(255,255,255,0.3);
}
.c11 {
appearance: none;
border: none;
border-radius: 4px;
box-shadow: inset 0 2px 4px rgba(0,0,0,.24);
box-sizing: border-box;
display: block;
height: 40px;
font-size: 16px;
padding: 0 16px;
outline: none;
width: 100%;
color: #324148;
background-color: #FFFFFF;
}
.c11::-ms-clear {
display: none;
}
.c11::placeholder {
opacity: 0.4;
}
.c11:read-only {
cursor: not-allowed;
}
.c10 {
color: #FFFFFF;
display: block;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
width: 100%;
margin-bottom: 4px;
}
.c6 {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 300;
font-size: 22px;
line-height: 32px;
text-transform: uppercase;
margin: 0px;
margin-right: auto;
color: rgba(255,255,255,0.87);
}
.c13 {
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
margin-bottom: 16px;
}
.c14 {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
margin: 0px;
}
.c16 {
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
margin-right: 16px;
}
.c4 {
box-sizing: border-box;
flex: 1;
display: flex;
flex-direction: column;
}
.c5 {
box-sizing: border-box;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.c8 {
box-sizing: border-box;
display: flex;
align-items: center;
flex-direction: row;
}
.c15 {
box-sizing: border-box;
margin-bottom: 8px;
padding: 8px;
background-color: #010B1C;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
}
.c1 {
z-index: -1;
position: fixed;
right: 0;
bottom: 0;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5);
opacity: 1;
touch-action: none;
}
.c0 {
position: fixed;
z-index: 1200;
right: 0;
bottom: 0;
top: 0;
left: 0;
}
.c2 {
height: 100%;
outline: none;
color: black;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
will-change: opacity;
transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms;
}
.c3 {
padding: 32px;
padding-top: 24px;
background: #1C254D;
color: rgba(255,255,255,0.87);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0,0,0,0.24);
display: flex;
flex-direction: column;
position: relative;
overflow-y: auto;
max-height: calc(100% - 96px);
max-width: 600px;
width: 100%;
min-height: 330px;
}
.c7 {
box-sizing: border-box;
margin-bottom: 32px;
min-height: 254px;
flex: 0 0 auto;
display: flex;
flex-direction: column;
}
.c18 {
box-sizing: border-box;
}
<div
class="c0"
data-testid="Modal"
>
<div
aria-hidden="true"
class="c1"
data-testid="backdrop"
/>
<div
class="c2"
>
<div
class="c3"
data-testid="dialogbox"
>
<div
class="c4"
>
<div
class="c5"
>
<div
class="c6"
color="text.primary"
>
Add Application
</div>
</div>
<div
class="c7"
>
<div
class="c8"
>
<div
class="c9"
width="320px"
>
<label
class="c10"
font-size="0"
>
App Name
</label>
<input
autocomplete="off"
class="c11"
color="text.onLight"
placeholder="jenkins"
type="text"
value=""
/>
</div>
<div
class="c12"
width="100%"
>
<label
class="c10"
font-size="0"
>
INTERNAL APPLICATION URL
</label>
<input
autocomplete="off"
class="c11"
color="text.onLight"
placeholder="https://localhost:4000"
type="text"
value=""
/>
</div>
</div>
<div
class="c13"
>
Use the script below to add an application to your cluster.
<br />
The script will be valid for
<span
class="c14"
>
1 hour
.
</span>
</div>
<div
class="c15"
>
<div
class="c16"
style="word-break: break-all; font-size: 12px; font-family: \\"Droid Sans Mono\\", \\"monospace\\", monospace, \\"Droid Sans Fallback\\";"
>
sudo bash -c "$(curl -fsSL 'http://localhost/scripts/86/install-app.sh?name=test&uri=http://myapp/')"
</div>
<button
class="c17"
kind="primary"
style="padding: 4px 8px; min-height: 10px; font-size: 10px;"
>
Copy
</button>
</div>
</div>
<div
class="c18"
>
<button
class="c19"
kind="primary"
>
Regenerate
</button>
<button
class="c20"
kind="secondary"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`loaded 1`] = `
.c9 {
box-sizing: border-box;
margin-bottom: 24px;
margin-right: 16px;
width: 320px;
}
.c12 {
box-sizing: border-box;
margin-bottom: 24px;
width: 100%;
}
.c15 {
line-height: 1.5;
margin: 0;
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
outline: none;
position: relative;
text-align: center;
text-decoration: none;
text-transform: uppercase;
transition: all 0.3s;
-webkit-font-smoothing: antialiased;
background: #512FC9;
color: rgba(255,255,255,0.87);
min-height: 32px;
font-size: 12px;
padding: 0px 24px;
margin-right: 16px;
}
.c15:active {
opacity: 0.56;
}
.c15:hover,
.c15:focus {
background: #651FFF;
}
.c15:active {
background: #354AA4;
}
.c15:disabled {
background: rgba(255,255,255,0.12);
color: rgba(255,255,255,0.3);
}
.c16 {
line-height: 1.5;
margin: 0;
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
outline: none;
position: relative;
text-align: center;
text-decoration: none;
text-transform: uppercase;
transition: all 0.3s;
-webkit-font-smoothing: antialiased;
background: #222C59;
color: rgba(255,255,255,0.87);
min-height: 32px;
font-size: 12px;
padding: 0px 24px;
}
.c16:active {
opacity: 0.56;
}
.c16:hover,
.c16:focus {
background: #2C3A73;
}
.c16:disabled {
background: rgba(255,255,255,0.12);
color: rgba(255,255,255,0.3);
}
.c11 {
appearance: none;
border: none;
border-radius: 4px;
box-shadow: inset 0 2px 4px rgba(0,0,0,.24);
box-sizing: border-box;
display: block;
height: 40px;
font-size: 16px;
padding: 0 16px;
outline: none;
width: 100%;
color: #324148;
background-color: #FFFFFF;
}
.c11::-ms-clear {
display: none;
}
.c11::placeholder {
opacity: 0.4;
}
.c11:read-only {
cursor: not-allowed;
}
.c10 {
color: #FFFFFF;
display: block;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
width: 100%;
margin-bottom: 4px;
}
.c6 {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 300;
font-size: 22px;
line-height: 32px;
text-transform: uppercase;
margin: 0px;
margin-right: auto;
color: rgba(255,255,255,0.87);
}
.c13 {
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
margin-bottom: 16px;
}
.c4 {
box-sizing: border-box;
flex: 1;
display: flex;
flex-direction: column;
}
.c5 {
box-sizing: border-box;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.c8 {
box-sizing: border-box;
display: flex;
align-items: center;
flex-direction: row;
}
.c1 {
z-index: -1;
position: fixed;
right: 0;
bottom: 0;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5);
opacity: 1;
touch-action: none;
}
.c0 {
position: fixed;
z-index: 1200;
right: 0;
bottom: 0;
top: 0;
left: 0;
}
.c2 {
height: 100%;
outline: none;
color: black;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
will-change: opacity;
transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms;
}
.c3 {
padding: 32px;
padding-top: 24px;
background: #1C254D;
color: rgba(255,255,255,0.87);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0,0,0,0.24);
display: flex;
flex-direction: column;
position: relative;
overflow-y: auto;
max-height: calc(100% - 96px);
max-width: 600px;
width: 100%;
min-height: 330px;
}
.c7 {
box-sizing: border-box;
margin-bottom: 32px;
min-height: 254px;
flex: 0 0 auto;
display: flex;
flex-direction: column;
}
.c14 {
box-sizing: border-box;
}
<div
class="c0"
data-testid="Modal"
>
<div
aria-hidden="true"
class="c1"
data-testid="backdrop"
/>
<div
class="c2"
>
<div
class="c3"
data-testid="dialogbox"
>
<div
class="c4"
>
<div
class="c5"
>
<div
class="c6"
color="text.primary"
>
Add Application
</div>
</div>
<div
class="c7"
>
<div
class="c8"
>
<div
class="c9"
width="320px"
>
<label
class="c10"
font-size="0"
>
App Name
</label>
<input
autocomplete="off"
class="c11"
color="text.onLight"
placeholder="jenkins"
type="text"
value=""
/>
</div>
<div
class="c12"
width="100%"
>
<label
class="c10"
font-size="0"
>
INTERNAL APPLICATION URL
</label>
<input
autocomplete="off"
class="c11"
color="text.onLight"
placeholder="https://localhost:4000"
type="text"
value=""
/>
</div>
</div>
<div
class="c13"
>
Provide the name and URL of your application to generate a script that will automatically install and configure the App service on the server that can access your application.
</div>
</div>
<div
class="c14"
>
<button
class="c15"
kind="primary"
>
Generate Script
</button>
<button
class="c16"
kind="secondary"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,19 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 AddApp from './AddApp';
export default AddApp;

View file

@ -0,0 +1,50 @@
/*
Copyright 2020 Gravitational, Inc.
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 { useState } from 'react';
import TeleportContext from 'teleport/teleportContext';
import useAttempt from 'shared/hooks/useAttemptNext';
export default function useAddApp(ctx: TeleportContext) {
const { attempt, run } = useAttempt('');
const canCreateToken = ctx.storeUser.getTokenAccess().create;
const version = ctx.storeUser.state.cluster.authVersion;
const [automatic, setAutomatic] = useState(true);
const [cmd, setCmd] = useState('');
const [expires, setExpires] = useState('');
function createToken(appName = '', appUri = '') {
return run(() =>
ctx.nodeService.createAppBashCommand(appName, appUri).then(result => {
setCmd(result.text);
setExpires(result.expires);
})
);
}
return {
canCreateToken,
version,
createToken,
cmd,
expires,
attempt,
automatic,
setAutomatic,
};
}
export type State = ReturnType<typeof useAddApp>;

View file

@ -0,0 +1,106 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import styled from 'styled-components';
import { Text, Flex } from 'design';
import { MenuIcon, MenuItem } from 'shared/components/MenuAction';
import { App } from 'teleport/services/apps';
import { NewTab } from 'design/Icon';
export default function AppList({ apps = [] }: Props) {
const $apps = apps.map(app => <Item mb={4} mr={5} app={app} key={app.id} />);
return <Flex flexWrap="wrap">{$apps}</Flex>;
}
function Item(props: ItemProps) {
const { app, ...rest } = props;
return (
<StyledAppListItem
width="240px"
height="240px"
borderRadius="3"
flexDirection="column"
alignItems="center"
justifyContent="center"
bg="primary.light"
{...rest}
>
<Flex width="100%" justifyContent="center">
<MenuIcon buttonIconProps={menuActionProps}>
<MenuItem as="a" href={app.launchUrl} target="_blank">
Open
</MenuItem>
</MenuIcon>
</Flex>
<Flex
flex="1"
alignItems="center"
justifyContent="center"
flexDirection="column"
as="a"
tabIndex={-1}
target="_blank"
color="text.primary"
href={app.launchUrl}
width="220px"
px="2"
style={{
textDecoration: 'none',
}}
>
<NewTab fontSize="62px" mb="3" />
<Text style={textStyle} bold mb="2">
{app.name}
</Text>
</Flex>
</StyledAppListItem>
);
}
const textStyle = {
textAlign: 'center',
width: '100%',
};
const StyledAppListItem = styled(Flex)`
position: relative;
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.24);
cursor: pointer;
transition: 0.3s;
border: 2px solid transparent;
&:hover {
border: 2px solid ${props => props.theme.colors.secondary.main};
background: ${props => props.theme.colors.primary.lighter};
}
`;
type Props = {
apps: App[];
};
type ItemProps = {
app: App;
[name: string]: any;
};
const menuActionProps = {
style: {
right: '10px',
position: 'absolute',
top: '10px',
},
};

View file

@ -0,0 +1,18 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 AppList from './AppList';
export default AppList;

View file

@ -0,0 +1,16 @@
import React from 'react';
import { Loaded, Empty } from './Apps.story';
import { render } from 'design/utils/testing';
test('loaded state', async () => {
const { container, findAllByText } = render(<Loaded />);
await findAllByText(/Applications/i);
expect(container).toMatchSnapshot();
});
test('empty state', async () => {
const { container, findByText } = render(<Empty />);
await findByText(/secure your first application/i);
expect(container).toMatchSnapshot();
});

View file

@ -0,0 +1,98 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import DefaultApps from './Apps';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router';
import makeAcl from 'teleport/services/user/makeAcl';
import { apps } from './fixtures';
import { ContextProvider, Context } from 'teleport';
export default {
title: 'Teleport/Apps',
};
export const Loaded = () => {
const ctx = new Context();
const acl = makeAcl(sample.acl);
ctx.isEnterprise = true;
ctx.storeUser.setState({ acl });
ctx.appService.fetchApps = () => Promise.resolve(apps);
return render(ctx);
};
export const Empty = () => {
const ctx = new Context();
const acl = makeAcl(sample.acl);
ctx.isEnterprise = true;
ctx.storeUser.setState({ acl });
ctx.appService.fetchApps = () => Promise.resolve([]);
return render(ctx);
};
export const Processing = () => {
const ctx = new Context();
const acl = makeAcl(sample.acl);
ctx.isEnterprise = true;
ctx.storeUser.setState({ acl });
ctx.appService.fetchApps = () => new Promise(() => null);
return render(ctx);
};
export const Failed = () => {
const ctx = new Context();
const acl = makeAcl(sample.acl);
ctx.isEnterprise = true;
ctx.storeUser.setState({ acl });
ctx.appService.fetchApps = () =>
Promise.reject(new Error('some error message'));
return render(ctx);
};
function render(ctx) {
const history = createMemoryHistory({
initialEntries: ['/web/cluster/localhost/audit/events'],
initialIndex: 0,
});
return (
<ContextProvider ctx={ctx}>
<Router history={history}>
<DefaultApps />
</Router>
</ContextProvider>
);
}
const sample = {
acl: {
tokens: {
create: true,
},
apps: {
list: true,
create: true,
remove: true,
edit: true,
read: true,
},
},
};

View file

@ -0,0 +1,77 @@
/**
* Copyright 2020 Gravitational, Inc.
*
* 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 from 'react';
import {
FeatureBox,
FeatureHeader,
FeatureHeaderTitle,
} from 'teleport/components/Layout';
import { Danger } from 'design/Alert';
import { Indicator, Box } from 'design';
import AppList from './AppList';
import Empty from './Empty';
import AddApp from './AddApp';
import ButtonAdd from './ButtonAdd';
import useApps, { State } from './useApps';
export default function Container() {
const state = useApps();
return <Apps {...state} />;
}
export function Apps(props: State) {
const {
isEnterprise,
isAddAppVisible,
showAddApp,
hideAddApp,
canCreate,
attempt,
apps,
} = props;
const isEmpty = attempt.status === 'success' && apps.length === 0;
const hasApps = attempt.status === 'success' && apps.length > 0;
return (
<FeatureBox>
<FeatureHeader alignItems="center" justifyContent="space-between">
<FeatureHeaderTitle>Applications</FeatureHeaderTitle>
<ButtonAdd
isEnterprise={isEnterprise}
canCreate={canCreate}
onClick={showAddApp}
/>
</FeatureHeader>
{attempt.status === 'processing' && (
<Box textAlign="center" m={10}>
<Indicator />
</Box>
)}
{attempt.status === 'failed' && <Danger>{attempt.statusText} </Danger>}
{hasApps && <AppList apps={apps} />}
{isEmpty && (
<Empty
isEnterprise={isEnterprise}
canCreate={canCreate}
onCreate={showAddApp}
/>
)}
{isAddAppVisible && <AddApp onClose={hideAddApp} />}
</FeatureBox>
);
}

View file

@ -0,0 +1,57 @@
/*
Copyright 2020 Gravitational, Inc.
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 from 'react';
import { ButtonPrimary } from 'design';
const docUrl = 'https://gravitational.com/teleport/docs/';
export default function ButtonAdd(props: Props) {
if (!props.isEnterprise) {
return (
<ButtonPrimary
{...props}
width="240px"
onClick={() => null}
as="a"
target="_blank"
href={docUrl}
>
View documentation
</ButtonPrimary>
);
}
if (props.canCreate) {
return (
<ButtonPrimary width="240px" {...props}>
Add Application
</ButtonPrimary>
);
}
return null;
}
type Props = {
isEnterprise: boolean;
canCreate: boolean;
onClick?: () => void;
mb?: string;
mx?: string;
width?: string;
kind?: string;
};

View file

@ -0,0 +1,59 @@
/*
Copyright 2020 Gravitational, Inc.
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 from 'react';
import { Text, Box, Flex } from 'design';
import Card from 'design/Card';
import Image from 'design/Image';
import ButtonAdd from './../ButtonAdd';
import { emptyPng } from './assets';
export default function Empty(props: Props) {
return (
<Card maxWidth="700px" mx="auto" py={4} as={Flex} alignItems="center">
<Box mx="4">
<Image width="180px" src={emptyPng} />
</Box>
<Box>
<Box pr={4} mb={6}>
<Text typography="h6" mb={3}>
SECURE YOUR FIRST APPLICATION
</Text>
<Text mb={3}>
Teleport Application Access provides secure access to internal
applications without the need for a VPN and with the auditability
and control of Teleport.
</Text>
</Box>
<ButtonAdd
isEnterprise={props.isEnterprise}
canCreate={props.canCreate}
onClick={props.onCreate}
mb="2"
mx="auto"
width="240px"
kind="primary"
/>
</Box>
</Card>
);
}
type Props = {
isEnterprise: boolean;
canCreate: boolean;
onCreate(): void;
};

View file

@ -0,0 +1,2 @@
import emptyPng from './empty.png';
export { emptyPng };

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Some files were not shown because too many files have changed in this diff Show more