mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
Add downloads page (#1452)
This commit is contained in:
parent
f2a05c9187
commit
1604aae4ab
|
@ -20,45 +20,14 @@ import { MemoryRouter } from 'react-router';
|
|||
|
||||
import { render, screen } from 'design/utils/testing';
|
||||
|
||||
import { Access, Acl, makeUserContext } from 'teleport/services/user';
|
||||
import { Acl, makeUserContext } from 'teleport/services/user';
|
||||
import { getMockWebSession } from 'teleport/services/websession/test-utils';
|
||||
import TeleportContext from 'teleport/teleportContext';
|
||||
import TeleportContextProvider from 'teleport/TeleportContextProvider';
|
||||
import { Discover } from 'teleport/Discover/Discover';
|
||||
import { FeaturesContextProvider } from 'teleport/FeaturesContext';
|
||||
import { SessionContextProvider } from 'teleport/WebSessionContext';
|
||||
|
||||
const fullAccess: Access = {
|
||||
list: true,
|
||||
read: true,
|
||||
edit: true,
|
||||
create: true,
|
||||
remove: true,
|
||||
};
|
||||
|
||||
const fullAcl: Acl = {
|
||||
windowsLogins: ['Administrator'],
|
||||
tokens: fullAccess,
|
||||
appServers: fullAccess,
|
||||
kubeServers: fullAccess,
|
||||
recordedSessions: fullAccess,
|
||||
activeSessions: fullAccess,
|
||||
authConnectors: fullAccess,
|
||||
roles: fullAccess,
|
||||
users: fullAccess,
|
||||
trustedClusters: fullAccess,
|
||||
events: fullAccess,
|
||||
accessRequests: fullAccess,
|
||||
billing: fullAccess,
|
||||
dbServers: fullAccess,
|
||||
db: fullAccess,
|
||||
desktops: fullAccess,
|
||||
nodes: fullAccess,
|
||||
clipboardSharingEnabled: true,
|
||||
desktopSessionRecordingEnabled: true,
|
||||
directorySharingEnabled: true,
|
||||
connectionDiagnostic: fullAccess,
|
||||
};
|
||||
import { fullAcl } from 'teleport/mocks/contexts';
|
||||
|
||||
const userContextJson = {
|
||||
authType: 'sso',
|
||||
|
|
|
@ -21,44 +21,13 @@ import { MemoryRouter } from 'react-router';
|
|||
import { render, screen } from 'design/utils/testing';
|
||||
|
||||
import { SelectResource } from 'teleport/Discover/SelectResource/SelectResource';
|
||||
import { Access, Acl, makeUserContext } from 'teleport/services/user';
|
||||
import { Acl, makeUserContext } from 'teleport/services/user';
|
||||
import TeleportContext from 'teleport/teleportContext';
|
||||
import TeleportContextProvider from 'teleport/TeleportContextProvider';
|
||||
import { ResourceKind } from 'teleport/Discover/Shared';
|
||||
import { DiscoverProvider } from 'teleport/Discover/useDiscover';
|
||||
import { FeaturesContextProvider } from 'teleport/FeaturesContext';
|
||||
|
||||
const fullAccess: Access = {
|
||||
list: true,
|
||||
read: true,
|
||||
edit: true,
|
||||
create: true,
|
||||
remove: true,
|
||||
};
|
||||
|
||||
const fullAcl: Acl = {
|
||||
windowsLogins: ['Administrator'],
|
||||
tokens: fullAccess,
|
||||
appServers: fullAccess,
|
||||
kubeServers: fullAccess,
|
||||
recordedSessions: fullAccess,
|
||||
activeSessions: fullAccess,
|
||||
authConnectors: fullAccess,
|
||||
roles: fullAccess,
|
||||
users: fullAccess,
|
||||
trustedClusters: fullAccess,
|
||||
events: fullAccess,
|
||||
accessRequests: fullAccess,
|
||||
billing: fullAccess,
|
||||
dbServers: fullAccess,
|
||||
db: fullAccess,
|
||||
desktops: fullAccess,
|
||||
nodes: fullAccess,
|
||||
clipboardSharingEnabled: true,
|
||||
desktopSessionRecordingEnabled: true,
|
||||
directorySharingEnabled: true,
|
||||
connectionDiagnostic: fullAccess,
|
||||
};
|
||||
import { fullAccess, fullAcl } from 'teleport/mocks/contexts';
|
||||
|
||||
const userContextJson = {
|
||||
authType: 'sso',
|
||||
|
|
|
@ -14,40 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { fullAcl } from 'teleport/mocks/contexts';
|
||||
import makeUserContext from 'teleport/services/user/makeUserContext';
|
||||
import { Access, Acl } from 'teleport/services/user/types';
|
||||
|
||||
const fullAccess: Access = {
|
||||
list: true,
|
||||
read: true,
|
||||
edit: true,
|
||||
create: true,
|
||||
remove: true,
|
||||
};
|
||||
|
||||
export const fullAcl: Acl = {
|
||||
windowsLogins: ['Administrator'],
|
||||
tokens: fullAccess,
|
||||
appServers: fullAccess,
|
||||
kubeServers: fullAccess,
|
||||
recordedSessions: fullAccess,
|
||||
activeSessions: fullAccess,
|
||||
authConnectors: fullAccess,
|
||||
roles: fullAccess,
|
||||
users: fullAccess,
|
||||
trustedClusters: fullAccess,
|
||||
events: fullAccess,
|
||||
accessRequests: fullAccess,
|
||||
billing: fullAccess,
|
||||
dbServers: fullAccess,
|
||||
db: fullAccess,
|
||||
desktops: fullAccess,
|
||||
nodes: fullAccess,
|
||||
connectionDiagnostic: fullAccess,
|
||||
clipboardSharingEnabled: true,
|
||||
desktopSessionRecordingEnabled: true,
|
||||
directorySharingEnabled: true,
|
||||
};
|
||||
|
||||
export const userContext = makeUserContext({
|
||||
authType: 'sso',
|
||||
|
|
|
@ -150,5 +150,12 @@ const defaultProps = {
|
|||
],
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
Icon: Icons.Question,
|
||||
route: 'https://example.com',
|
||||
isExternalLink: true,
|
||||
items: [],
|
||||
},
|
||||
] as Item[],
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ import SideNavItem from './SideNavItem';
|
|||
import SideNavItemGroup from './SideNavItemGroup';
|
||||
import logoSvg from './logo';
|
||||
import useSideNav from './useSideNav';
|
||||
import SideNavExternalLink from './SideNavExternalLink';
|
||||
|
||||
export default function Container() {
|
||||
const state = useSideNav();
|
||||
|
@ -40,7 +41,11 @@ export function SideNav(props: ReturnType<typeof useSideNav>) {
|
|||
return <SideNavItemGroup path={path} item={item} key={index} />;
|
||||
}
|
||||
|
||||
return (
|
||||
return item.isExternalLink ? (
|
||||
<SideNavExternalLink key={index} icon={item.Icon} href={item.route}>
|
||||
{item.title}
|
||||
</SideNavExternalLink>
|
||||
) : (
|
||||
<SideNavItem key={index} as={NavLink} exact={item.exact} to={item.route}>
|
||||
<SideNavItemIcon as={item.Icon} />
|
||||
{item.title}
|
||||
|
|
44
web/packages/teleport/src/SideNav/SideNavExternalLink.tsx
Normal file
44
web/packages/teleport/src/SideNav/SideNavExternalLink.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Copyright 2023 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 { ArrowForward } from 'design/Icon';
|
||||
|
||||
import theme from 'design/theme';
|
||||
|
||||
import SideNavItemIcon from './SideNavItemIcon';
|
||||
import SideNavItem from './SideNavItem';
|
||||
|
||||
const SideNavExternalLink = ({ children, href, icon }) => {
|
||||
return (
|
||||
<SideNavItem
|
||||
as="a"
|
||||
href={href}
|
||||
target="_blank"
|
||||
css={{ paddingRight: `${theme.space[2]}px` }}
|
||||
>
|
||||
<SideNavItemIcon as={icon} />
|
||||
{children}
|
||||
<SideNavItemIcon
|
||||
css={{ marginLeft: 'auto', transform: 'rotate(-45deg)' }}
|
||||
as={ArrowForward}
|
||||
/>
|
||||
</SideNavItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideNavExternalLink;
|
|
@ -206,6 +206,63 @@ exports[`rendering of SideNav 1`] = `
|
|||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: none;
|
||||
border-left: 4px solid transparent;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-family: Ubuntu2,-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
||||
padding-left: 64px;
|
||||
padding-right: 32px;
|
||||
background: #222C59;
|
||||
color: rgba(255,255,255,0.56);
|
||||
min-height: 56px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.c15:active,
|
||||
.c15.active {
|
||||
border-left-color: #651FFF;
|
||||
background: #2C3A73;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.c15:active .marker,
|
||||
.c15.active .marker {
|
||||
background: #651FFF;
|
||||
}
|
||||
|
||||
.c15:hover {
|
||||
background: #2C3A73;
|
||||
}
|
||||
|
||||
.c15:focus,
|
||||
.c15:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
display: inline-block;
|
||||
transition: color 0.3s;
|
||||
margin-left: -40px;
|
||||
margin-right: 16px;
|
||||
color: inherit;
|
||||
font-size: 16px;
|
||||
margin-left: auto;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.c2 {
|
||||
box-sizing: border-box;
|
||||
padding-left: 24px;
|
||||
|
@ -471,6 +528,23 @@ exports[`rendering of SideNav 1`] = `
|
|||
Trust
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="c15"
|
||||
href="https://example.com"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="c6 icon icon-question-circle sc-AxjAm c7"
|
||||
color="inherit"
|
||||
font-size="16px"
|
||||
/>
|
||||
Support
|
||||
<span
|
||||
class="c6 icon icon-arrow_forward sc-AxjAm c16"
|
||||
color="inherit"
|
||||
font-size="16px"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
@ -52,6 +52,7 @@ function makeItems(clusterId: string, storeItems: Store.NavItem[]) {
|
|||
exact: cur.exact,
|
||||
title: cur.title,
|
||||
Icon: cur.Icon,
|
||||
isExternalLink: cur.isExternalLink,
|
||||
};
|
||||
|
||||
if (itemGroups[groupName]) {
|
||||
|
@ -105,4 +106,5 @@ export interface Item {
|
|||
exact?: boolean;
|
||||
title: string;
|
||||
Icon: any;
|
||||
isExternalLink?: boolean;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { generateTshLoginCommand, arrayStrDiff } from './util';
|
||||
import { generateTshLoginCommand, arrayStrDiff, compareSemVers } from './util';
|
||||
|
||||
let windowSpy;
|
||||
|
||||
|
@ -70,3 +70,29 @@ test('arrayStrDiff returns the correct diff', () => {
|
|||
|
||||
expect(arrayStrDiff(arrayA, arrayB)).toStrictEqual(['a', 'c', 'd']);
|
||||
});
|
||||
|
||||
test('compareSemVers', () => {
|
||||
expect(['3.0.0', '1.0.0', '2.0.0'].sort(compareSemVers)).toEqual([
|
||||
'1.0.0',
|
||||
'2.0.0',
|
||||
'3.0.0',
|
||||
]);
|
||||
|
||||
expect(['3.1.0', '3.2.0', '3.1.1'].sort(compareSemVers)).toEqual([
|
||||
'3.1.0',
|
||||
'3.1.1',
|
||||
'3.2.0',
|
||||
]);
|
||||
|
||||
expect(['10.0.1', '10.0.2', '2.0.0'].sort(compareSemVers)).toEqual([
|
||||
'2.0.0',
|
||||
'10.0.1',
|
||||
'10.0.2',
|
||||
]);
|
||||
|
||||
expect(['10.1.0', '11.1.0', '5.10.10'].sort(compareSemVers)).toEqual([
|
||||
'5.10.10',
|
||||
'10.1.0',
|
||||
'11.1.0',
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -85,3 +85,32 @@ export function arrayStrDiff(stringsA: string[], stringsB: string[]) {
|
|||
|
||||
return stringsA.filter(l => !stringsB.includes(l));
|
||||
}
|
||||
|
||||
export const compareSemVers = (a: string, b: string): -1 | 1 => {
|
||||
const splitA = a.split('.');
|
||||
const splitB = b.split('.');
|
||||
|
||||
if (splitA.length < 3 || splitB.length < 3) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const majorA = parseInt(splitA[0]);
|
||||
const majorB = parseInt(splitB[0]);
|
||||
if (majorA !== majorB) {
|
||||
return majorA > majorB ? 1 : -1;
|
||||
}
|
||||
|
||||
const minorA = parseInt(splitA[1]);
|
||||
const minorB = parseInt(splitB[1]);
|
||||
if (minorA !== minorB) {
|
||||
return minorA > minorB ? 1 : -1;
|
||||
}
|
||||
|
||||
const patchA = parseInt(splitA[2].split('-')[0]);
|
||||
const patchB = parseInt(splitB[2].split('-')[0]);
|
||||
if (patchA !== patchB) {
|
||||
return patchA > patchB ? 1 : -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
|
|
@ -49,6 +49,8 @@ export const fullAcl: Acl = {
|
|||
clipboardSharingEnabled: true,
|
||||
desktopSessionRecordingEnabled: true,
|
||||
directorySharingEnabled: true,
|
||||
license: fullAccess,
|
||||
download: fullAccess,
|
||||
};
|
||||
|
||||
export const userContext = makeUserContext({
|
||||
|
|
|
@ -51,6 +51,8 @@ export default function makeAcl(json): Acl {
|
|||
json.directorySharing !== undefined ? json.directorySharing : true;
|
||||
|
||||
const nodes = json.nodes || defaultAccess;
|
||||
const license = json.license || defaultAccess;
|
||||
const download = json.download || defaultAccess;
|
||||
|
||||
return {
|
||||
windowsLogins,
|
||||
|
@ -74,6 +76,8 @@ export default function makeAcl(json): Acl {
|
|||
nodes,
|
||||
directorySharingEnabled,
|
||||
connectionDiagnostic,
|
||||
license,
|
||||
download,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@ export interface Acl {
|
|||
desktops: Access;
|
||||
nodes: Access;
|
||||
connectionDiagnostic: Access;
|
||||
license: Access;
|
||||
download: Access;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
|
|
@ -128,6 +128,20 @@ test('undefined values in context response gives proper default values', async (
|
|||
create: false,
|
||||
remove: false,
|
||||
},
|
||||
license: {
|
||||
list: false,
|
||||
read: false,
|
||||
edit: false,
|
||||
create: false,
|
||||
remove: false,
|
||||
},
|
||||
download: {
|
||||
list: false,
|
||||
read: false,
|
||||
edit: false,
|
||||
create: false,
|
||||
remove: false,
|
||||
},
|
||||
tokens: {
|
||||
list: false,
|
||||
read: false,
|
||||
|
|
|
@ -68,5 +68,6 @@ export type NavItem = {
|
|||
Icon: any;
|
||||
exact?: boolean;
|
||||
getLink(clusterId?: string): string;
|
||||
isExternalLink?: boolean;
|
||||
group?: NavGroup;
|
||||
};
|
||||
|
|
|
@ -122,6 +122,14 @@ export default class StoreUserContext extends Store<UserContext> {
|
|||
return this.state.accessRequestId;
|
||||
}
|
||||
|
||||
getLicenceAccess() {
|
||||
return this.state.acl.license;
|
||||
}
|
||||
|
||||
getDownloadAccess() {
|
||||
return this.state.acl.download;
|
||||
}
|
||||
|
||||
getAccessRequestAccess() {
|
||||
return this.state.acl.accessRequests;
|
||||
}
|
||||
|
@ -134,6 +142,14 @@ export default class StoreUserContext extends Store<UserContext> {
|
|||
return tokens.create;
|
||||
}
|
||||
|
||||
// hasDownloadCenterListAccess checks if the user
|
||||
// has access to download either teleport binaries or the license.
|
||||
// Since the page is used to download both of them, having access to one
|
||||
// is enough to show access this page.
|
||||
hasDownloadCenterListAccess() {
|
||||
return this.state.acl.license.read || this.state.acl.download.list;
|
||||
}
|
||||
|
||||
// hasAccessToAgentQuery checks for at least one valid query permission.
|
||||
// Nodes require only a 'list' access while the rest of the agents
|
||||
// require 'list + read'.
|
||||
|
|
Loading…
Reference in a new issue