Add downloads page (#1452)

This commit is contained in:
matheus 2023-01-10 14:11:16 -03:00 committed by GitHub
parent f2a05c9187
commit 1604aae4ab
16 changed files with 233 additions and 101 deletions

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -150,5 +150,12 @@ const defaultProps = {
],
route: '',
},
{
title: 'Support',
Icon: Icons.Question,
route: 'https://example.com',
isExternalLink: true,
items: [],
},
] as Item[],
};

View file

@ -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}

View 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;

View file

@ -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>

View file

@ -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;
}

View file

@ -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',
]);
});

View file

@ -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;
};

View file

@ -49,6 +49,8 @@ export const fullAcl: Acl = {
clipboardSharingEnabled: true,
desktopSessionRecordingEnabled: true,
directorySharingEnabled: true,
license: fullAccess,
download: fullAccess,
};
export const userContext = makeUserContext({

View file

@ -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,
};
}

View file

@ -69,6 +69,8 @@ export interface Acl {
desktops: Access;
nodes: Access;
connectionDiagnostic: Access;
license: Access;
download: Access;
}
export interface User {

View file

@ -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,

View file

@ -68,5 +68,6 @@ export type NavItem = {
Icon: any;
exact?: boolean;
getLink(clusterId?: string): string;
isExternalLink?: boolean;
group?: NavGroup;
};

View file

@ -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'.