mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 17:23:22 +00:00
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:
parent
08e3c69712
commit
43d2a39a21
1
web/.gitignore
vendored
1
web/.gitignore
vendored
|
@ -3,5 +3,4 @@ node_modules
|
|||
coverage
|
||||
dist
|
||||
**/dist
|
||||
packages/force/dist
|
||||
!packages/gravity/dist
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -50,6 +50,10 @@ const Input = styled.input`
|
|||
opacity: 0.4;
|
||||
}
|
||||
|
||||
:read-only {
|
||||
cursor: not-allowed
|
||||
}
|
||||
|
||||
${color} ${space} ${width} ${height} ${error};
|
||||
`;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 152 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
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 |
|
@ -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);
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
src/proto
|
|
@ -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;
|
|
@ -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
|
||||
```
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import Ticker from './Ticker';
|
||||
export default Ticker;
|
|
@ -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);
|
41
web/packages/force/src/proto/tick_pb.d.ts
vendored
41
web/packages/force/src/proto/tick_pb.d.ts
vendored
|
@ -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 = {
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
}
|
|
@ -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;
|
|
@ -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(() => {
|
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
import events from './bpf.troubleshoot.json';
|
||||
export default events;
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'> & {
|
||||
|
|
|
@ -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 : '',
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
56
web/packages/shared/hooks/useAttemptNext.ts
Normal file
56
web/packages/shared/hooks/useAttemptNext.ts
Normal 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>;
|
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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',
|
228
web/packages/teleport/src/AccessStrategy/AccessStrategy.test.tsx
Normal file
228
web/packages/teleport/src/AccessStrategy/AccessStrategy.test.tsx
Normal 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',
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -39,7 +39,7 @@ export default function RequestError({ err }: Props) {
|
|||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<ButtonSecondary onClick={() => session.logout()}>
|
||||
Cancel & Logout
|
||||
{`Cancel & Logout`}
|
||||
</ButtonSecondary>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
|
@ -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;
|
|
@ -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 />
|
|
@ -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;
|
|
@ -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)}
|
|
@ -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"
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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} />;
|
||||
}
|
|
@ -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="" />;
|
||||
};
|
37
web/packages/teleport/src/AppLauncher/AppLauncher.tsx
Normal file
37
web/packages/teleport/src/AppLauncher/AppLauncher.tsx
Normal 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>
|
||||
);
|
||||
}
|
18
web/packages/teleport/src/AppLauncher/index.ts
Normal file
18
web/packages/teleport/src/AppLauncher/index.ts
Normal 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;
|
49
web/packages/teleport/src/AppLauncher/useAppLauncher.ts
Normal file
49
web/packages/teleport/src/AppLauncher/useAppLauncher.ts
Normal 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,
|
||||
};
|
||||
}
|
29
web/packages/teleport/src/Apps/AddApp/AddApp.story.test.tsx
Normal file
29
web/packages/teleport/src/Apps/AddApp/AddApp.story.test.tsx
Normal 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();
|
||||
});
|
72
web/packages/teleport/src/Apps/AddApp/AddApp.story.tsx
Normal file
72
web/packages/teleport/src/Apps/AddApp/AddApp.story.tsx
Normal 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,
|
||||
},
|
||||
};
|
87
web/packages/teleport/src/Apps/AddApp/AddApp.tsx
Normal file
87
web/packages/teleport/src/Apps/AddApp/AddApp.tsx
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
};
|
19
web/packages/teleport/src/Apps/AddApp/Automatically/index.ts
Normal file
19
web/packages/teleport/src/Apps/AddApp/Automatically/index.ts
Normal 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;
|
85
web/packages/teleport/src/Apps/AddApp/Manually/Manually.tsx
Normal file
85
web/packages/teleport/src/Apps/AddApp/Manually/Manually.tsx
Normal 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;
|
||||
};
|
19
web/packages/teleport/src/Apps/AddApp/Manually/index.ts
Normal file
19
web/packages/teleport/src/Apps/AddApp/Manually/index.ts
Normal 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;
|
|
@ -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>
|
||||
`;
|
19
web/packages/teleport/src/Apps/AddApp/index.ts
Normal file
19
web/packages/teleport/src/Apps/AddApp/index.ts
Normal 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;
|
50
web/packages/teleport/src/Apps/AddApp/useAddApp.ts
Normal file
50
web/packages/teleport/src/Apps/AddApp/useAddApp.ts
Normal 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>;
|
106
web/packages/teleport/src/Apps/AppList/AppList.tsx
Normal file
106
web/packages/teleport/src/Apps/AppList/AppList.tsx
Normal 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',
|
||||
},
|
||||
};
|
18
web/packages/teleport/src/Apps/AppList/index.ts
Normal file
18
web/packages/teleport/src/Apps/AppList/index.ts
Normal 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;
|
16
web/packages/teleport/src/Apps/Apps.story.test.tsx
Normal file
16
web/packages/teleport/src/Apps/Apps.story.test.tsx
Normal 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();
|
||||
});
|
98
web/packages/teleport/src/Apps/Apps.story.tsx
Normal file
98
web/packages/teleport/src/Apps/Apps.story.tsx
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
77
web/packages/teleport/src/Apps/Apps.tsx
Normal file
77
web/packages/teleport/src/Apps/Apps.tsx
Normal 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>
|
||||
);
|
||||
}
|
57
web/packages/teleport/src/Apps/ButtonAdd.tsx
Normal file
57
web/packages/teleport/src/Apps/ButtonAdd.tsx
Normal 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;
|
||||
};
|
59
web/packages/teleport/src/Apps/Empty/Empty.tsx
Normal file
59
web/packages/teleport/src/Apps/Empty/Empty.tsx
Normal 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;
|
||||
};
|
2
web/packages/teleport/src/Apps/Empty/assets.js
Normal file
2
web/packages/teleport/src/Apps/Empty/assets.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import emptyPng from './empty.png';
|
||||
export { emptyPng };
|
BIN
web/packages/teleport/src/Apps/Empty/empty.png
Normal file
BIN
web/packages/teleport/src/Apps/Empty/empty.png
Normal file
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
Loading…
Reference in a new issue