Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4f568e7
Add locked feature options
mcbattirola May 3, 2023
833d056
Merge branch 'master' of github.com:gravitational/teleport into mcbat…
mcbattirola May 3, 2023
fcb5b7d
Merge branch 'master' of github.com:gravitational/teleport into mcbat…
mcbattirola May 4, 2023
5bd6527
Remove routes from items with locked parents
mcbattirola May 4, 2023
3e5d91e
Refactor to simplify features
mcbattirola May 5, 2023
6c8d020
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 5, 2023
2ee0447
Add type for LockedFeatures
mcbattirola May 5, 2023
f02ff17
Improve comment
mcbattirola May 5, 2023
b2f51d4
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 5, 2023
da6c8f0
Fix call to isLockedAndUpdatedRouteAndNavigationItem wrong param
mcbattirola May 5, 2023
604d25b
Merge branch 'mcbattirola/allow-locked-features' of github.com:gravit…
mcbattirola May 5, 2023
1a6578d
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 5, 2023
7c547eb
Merge branch 'master' of github.com:gravitational/teleport into mcbat…
mcbattirola May 8, 2023
905845a
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 8, 2023
77f8e68
Merge branch 'mcbattirola/allow-locked-features' of github.com:gravit…
mcbattirola May 8, 2023
35ca4aa
Set locked features based on usage based billing
mcbattirola May 8, 2023
b3aa84a
Add back lockedRoute and list items
mcbattirola May 8, 2023
41bd12d
Use simpler sintax
mcbattirola May 8, 2023
f7ee19e
Simplify isParentLocked
mcbattirola May 8, 2023
d9f1e98
Improve navigation item rendering
mcbattirola May 8, 2023
5281695
Remove unucessary condition from if
mcbattirola May 8, 2023
8aa01ce
Prevent adding a route if the feature is locked
mcbattirola May 8, 2023
0c4015e
Throw error instead of logging
mcbattirola May 8, 2023
26da42a
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 8, 2023
f6c1852
Improve if condition
mcbattirola May 8, 2023
9320bf5
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 8, 2023
f96af8d
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 8, 2023
450dfc8
Merge branch 'master' into mcbattirola/allow-locked-features
mcbattirola May 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions web/packages/teleport/src/Main/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { MainContainer } from './MainContainer';
import { OnboardDiscover } from './OnboardDiscover';

import type { BannerType } from 'teleport/components/BannerList/BannerList';
import type { TeleportFeature } from 'teleport/types';
import type { LockedFeatures, TeleportFeature } from 'teleport/types';

interface MainProps {
initialAlerts?: ClusterAlert[];
Expand Down Expand Up @@ -156,7 +156,7 @@ export function Main(props: MainProps) {
<ContentMinWidth>
<Suspense fallback={null}>
<TopBar />
<FeatureRoutes />
<FeatureRoutes lockedFeatures={ctx.lockedFeatures} />
</Suspense>
</ContentMinWidth>
</HorizontalSplit>
Expand All @@ -169,13 +169,47 @@ export function Main(props: MainProps) {
);
}

function renderRoutes(features: TeleportFeature[]) {
function renderRoutes(
features: TeleportFeature[],
lockedFeatures: LockedFeatures
) {
const routes = [];

for (const [index, feature] of features.entries()) {
const isParentLocked =
feature.parent && new feature.parent().isLocked?.(lockedFeatures);

// remove features with parents locked.
// The parent itself will be rendered if it has a lockedRoute,
// but the children shouldn't be.
if (isParentLocked) {
continue;
}

// add the route of the 'locked' variants of the features
if (feature.isLocked?.(lockedFeatures)) {
if (!feature.lockedRoute) {
throw new Error('a locked feature without a locked route was found');
}

const { path, title, exact, component: Component } = feature.lockedRoute;
routes.push(
<Route title={title} key={index} path={path} exact={exact}>
<CatchError>
<Suspense fallback={null}>
<Component />
</Suspense>
</CatchError>
</Route>
);

// return early so we don't add the original route
continue;
}

// add regular feature routes
if (feature.route) {
const { path, title, exact, component: Component } = feature.route;

routes.push(
<Route title={title} key={index} path={path} exact={exact}>
<CatchError>
Expand All @@ -191,9 +225,9 @@ function renderRoutes(features: TeleportFeature[]) {
return routes;
}

function FeatureRoutes() {
function FeatureRoutes({ lockedFeatures }: { lockedFeatures: LockedFeatures }) {
const features = useFeatures();
const routes = renderRoutes(features);
const routes = renderRoutes(features, lockedFeatures);

return <Switch>{routes}</Switch>;
}
Expand Down
56 changes: 40 additions & 16 deletions web/packages/teleport/src/Navigation/NavigationItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ import { generatePath, Router } from 'react-router';

import { createMemoryHistory } from 'history';

import TeleportContextProvider from 'teleport/TeleportContextProvider';
import TeleportContext from 'teleport/teleportContext';

import { TeleportFeature } from 'teleport/types';
import { NavigationCategory } from 'teleport/Navigation/categories';
import { NavigationItem } from 'teleport/Navigation/NavigationItem';
import { NavigationItemSize } from 'teleport/Navigation/common';
import { makeUserContext } from 'teleport/services/user';

class MockFeature implements TeleportFeature {
category = NavigationCategory.Resources;
Expand Down Expand Up @@ -57,15 +61,25 @@ describe('navigation items', () => {
initialEntries: ['/web/cluster/root/feature'],
});

const ctx = new TeleportContext();
ctx.storeUser.state = makeUserContext({
cluster: {
name: 'test-cluster',
lastConnected: Date.now(),
},
});

render(
<Router history={history}>
<NavigationItem
feature={new MockFeature()}
size={NavigationItemSize.Large}
transitionDelay={100}
visible={true}
/>
</Router>
<TeleportContextProvider ctx={ctx}>
<Router history={history}>
<NavigationItem
feature={new MockFeature()}
size={NavigationItemSize.Large}
transitionDelay={100}
visible={true}
/>
</Router>
</TeleportContextProvider>
);

expect(screen.getByText('Some Feature').closest('a')).toHaveAttribute(
Expand All @@ -79,15 +93,25 @@ describe('navigation items', () => {
initialEntries: ['/web/cluster/root/feature'],
});

const ctx = new TeleportContext();
ctx.storeUser.state = makeUserContext({
cluster: {
name: 'test-cluster',
lastConnected: Date.now(),
},
});

render(
<Router history={history}>
<NavigationItem
feature={new MockFeature()}
size={NavigationItemSize.Large}
transitionDelay={100}
visible={true}
/>
</Router>
<TeleportContextProvider ctx={ctx}>
<Router history={history}>
<NavigationItem
feature={new MockFeature()}
size={NavigationItemSize.Large}
transitionDelay={100}
visible={true}
/>
</Router>
</TeleportContextProvider>
);

expect(screen.getByText('Some Feature').closest('a')).toHaveAttribute(
Expand Down
32 changes: 27 additions & 5 deletions web/packages/teleport/src/Navigation/NavigationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ import {

import useStickyClusterId from 'teleport/useStickyClusterId';

import type { TeleportFeature } from 'teleport/types';
import { useTeleport } from 'teleport';

import type {
TeleportFeature,
TeleportFeatureNavigationItem,
} from 'teleport/types';

interface NavigationItemProps {
feature: TeleportFeature;
Expand Down Expand Up @@ -76,9 +81,11 @@ const ExternalLinkIndicator = styled.div`
`;

export function NavigationItem(props: NavigationItemProps) {
const ctx = useTeleport();
const { clusterId } = useStickyClusterId();

const { navigationItem, route } = props.feature;
const { navigationItem, route, isLocked, lockedNavigationItem, lockedRoute } =
props.feature;

const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
Expand Down Expand Up @@ -179,18 +186,33 @@ export function NavigationItem(props: NavigationItemProps) {
);
}

let navigationItemVersion: TeleportFeatureNavigationItem;
if (route) {
navigationItemVersion = navigationItem;
}

// use locked item version if feature is locked
if (lockedRoute && isLocked?.(ctx.lockedFeatures)) {
if (!lockedNavigationItem) {
throw new Error(
'locked feature without an alternative navigation item'
);
}
navigationItemVersion = lockedNavigationItem;
}

if (navigationItemVersion) {
return (
<Link
{...linkProps}
onKeyDown={handleKeyDown}
tabIndex={props.visible ? 0 : -1}
to={navigationItem.getLink(clusterId)}
exact={navigationItem.exact}
to={navigationItemVersion.getLink(clusterId)}
exact={navigationItemVersion.exact}
>
<LinkContent size={props.size}>
{getIcon(props.feature, props.size)}
{navigationItem.title}
{navigationItemVersion.title}
</LinkContent>
</Link>
);
Expand Down
2 changes: 1 addition & 1 deletion web/packages/teleport/src/Sessions/useSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ export default function useSessions(ctx: Ctx, clusterId: string) {
return {
attempt,
sessions,
showActiveSessionsCTA: ctx.ctas.activeSessions,
showActiveSessionsCTA: ctx.lockedFeatures.activeSessions,
};
}
2 changes: 1 addition & 1 deletion web/packages/teleport/src/Support/Support.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function Container({
isEnterprise={cfg.isEnterprise}
tunnelPublicAddress={cfg.tunnelPublicAddress}
isCloud={cfg.isCloud}
showPremiumSupportCTA={ctx.ctas.premiumSupport}
showPremiumSupportCTA={ctx.lockedFeatures.premiumSupport}
children={children}
/>
);
Expand Down
17 changes: 10 additions & 7 deletions web/packages/teleport/src/teleportContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,16 @@ class TeleportContext implements types.Context {
automaticUpgradesEnabled = false;
agentService = agentService;

// No CTA is currently shown
ctas = {
authConnectors: false,
activeSessions: false,
accessRequests: false,
premiumSupport: false,
trustedDevices: false,
// lockedFeatures are the features disabled in the user's cluster.
// Mainly used to hide features and/or show CTAs when the user cluster doesn't support it.
// TODO(mcbattirola): use cluster features instead of only using `isUsageBasedBilling`
// to determine which feature is locked
lockedFeatures: types.LockedFeatures = {
authConnectors: cfg.isUsageBasedBilling,
activeSessions: cfg.isUsageBasedBilling,
accessRequests: cfg.isUsageBasedBilling,
premiumSupport: cfg.isUsageBasedBilling,
trustedDevices: cfg.isUsageBasedBilling,
};

// init fetches data required for initial rendering of components.
Expand Down
24 changes: 22 additions & 2 deletions web/packages/teleport/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ export interface Context {
getFeatureFlags(): FeatureFlags;
}

interface TeleportFeatureNavigationItem {
export interface TeleportFeatureNavigationItem {
title: string;
icon: React.ReactNode;
exact?: boolean;
getLink?(clusterId: string): string;
isExternalLink?: boolean;
}

interface TeleportFeatureRoute {
export interface TeleportFeatureRoute {
title: string;
path: string;
exact?: boolean;
Expand All @@ -48,9 +48,20 @@ export interface TeleportFeature {
category?: NavigationCategory;
section?: ManagementSection;
hasAccess(flags: FeatureFlags): boolean;
// route defines react router Route fields.
// This field can be left undefined to indicate
// this feature is a parent to children features
// eg: FeatureAccessRequests is parent to sub features
// FeatureNewAccessRequest and FeatureReviewAccessRequests.
// These childrens will be responsible for routing.
route?: TeleportFeatureRoute;
navigationItem?: TeleportFeatureNavigationItem;
topMenuItem?: TeleportFeatureNavigationItem;
// alternative items to display when the user has permissions (RBAC)
// but the cluster lacks the feature:
isLocked?(lockedFeatures: LockedFeatures): boolean;
lockedNavigationItem?: TeleportFeatureNavigationItem;
lockedRoute?: TeleportFeatureRoute;
}

export type StickyCluster = {
Expand Down Expand Up @@ -97,3 +108,12 @@ export interface FeatureFlags {
locks: boolean;
newLocks: boolean;
}

// LockedFeatures are used for determining which features are disabled in the user's cluster.
export type LockedFeatures = {
authConnectors: boolean;
activeSessions: boolean;
accessRequests: boolean;
premiumSupport: boolean;
trustedDevices: boolean;
};