Skip to content

Commit

Permalink
web: support SAML resource deletion in unified resources view (#44311)
Browse files Browse the repository at this point in the history
* update SAML app edit mechanism to accomodate both edit and delete functions

* use user saml idp access to disable menu actions

* add comment to ResourceActionButton props

* refactor: SamlAppActionContext to edit and delete Saml application

* address review comments
  • Loading branch information
flyinghermit authored Jul 26, 2024
1 parent fa1a062 commit 0618056
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { nodes } from 'teleport/Nodes/fixtures';

import makeApp from 'teleport/services/apps/makeApps';
import { ResourceActionButton } from 'teleport/UnifiedResources/ResourceActionButton';
import { SamlAppActionProvider } from 'teleport/SamlApplications/useSamlAppActions';

import {
makeUnifiedResourceViewItemApp,
Expand Down Expand Up @@ -102,56 +103,51 @@ const ActionButton = <ButtonBorder size="small">Action</ButtonBorder>;
export const Cards: Story = {
render() {
return (
<Grid gap={2}>
{[
...apps.map(resource =>
makeUnifiedResourceViewItemApp(resource, {
ActionButton: (
<ResourceActionButton
resource={resource}
setResourceSpec={() =>
alert('Sets resource spec and opens update dialog')
}
/>
),
})
),
...databases.map(resource =>
makeUnifiedResourceViewItemDatabase(resource, {
ActionButton,
})
),
...kubes.map(resource =>
makeUnifiedResourceViewItemKube(resource, { ActionButton })
),
...nodes.map(resource =>
makeUnifiedResourceViewItemNode(resource, {
ActionButton,
})
),
...additionalResources.map(resource =>
makeUnifiedResourceViewItemApp(resource, { ActionButton })
),
...desktops.map(resource =>
makeUnifiedResourceViewItemDesktop(resource, { ActionButton })
),
].map((res, i) => (
<ResourceCard
key={i}
pinned={false}
pinResource={() => {}}
selectResource={() => {}}
selected={false}
pinningSupport={PinningSupport.Supported}
name={res.name}
primaryIconName={res.primaryIconName}
SecondaryIcon={res.SecondaryIcon}
cardViewProps={res.cardViewProps}
labels={res.labels}
ActionButton={res.ActionButton}
/>
))}
</Grid>
<SamlAppActionProvider>
<Grid gap={2}>
{[
...apps.map(resource =>
makeUnifiedResourceViewItemApp(resource, {
ActionButton: <ResourceActionButton resource={resource} />,
})
),
...databases.map(resource =>
makeUnifiedResourceViewItemDatabase(resource, {
ActionButton,
})
),
...kubes.map(resource =>
makeUnifiedResourceViewItemKube(resource, { ActionButton })
),
...nodes.map(resource =>
makeUnifiedResourceViewItemNode(resource, {
ActionButton,
})
),
...additionalResources.map(resource =>
makeUnifiedResourceViewItemApp(resource, { ActionButton })
),
...desktops.map(resource =>
makeUnifiedResourceViewItemDesktop(resource, { ActionButton })
),
].map((res, i) => (
<ResourceCard
key={i}
pinned={false}
pinResource={() => {}}
selectResource={() => {}}
selected={false}
pinningSupport={PinningSupport.Supported}
name={res.name}
primaryIconName={res.primaryIconName}
SecondaryIcon={res.SecondaryIcon}
cardViewProps={res.cardViewProps}
labels={res.labels}
ActionButton={res.ActionButton}
/>
))}
</Grid>
</SamlAppActionProvider>
);
},
};
2 changes: 1 addition & 1 deletion web/packages/teleport/src/Main/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ export const useNoMinWidth = () => {
}, []);
};

const ContentMinWidth = ({ children }: { children: ReactNode }) => {
export const ContentMinWidth = ({ children }: { children: ReactNode }) => {
const [enforceMinWidth, setEnforceMinWidth] = useState(true);

return (
Expand Down
118 changes: 118 additions & 0 deletions web/packages/teleport/src/SamlApplications/useSamlAppActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React, { createContext, useContext } from 'react';
import { Attempt } from 'shared/hooks/useAsync';

import { SamlMeta } from 'teleport/Discover/useDiscover';

import type { SamlAppToDelete } from 'teleport/services/samlidp/types';
import type { ResourceSpec } from 'teleport/Discover/SelectResource/types';
import type { Access } from 'teleport/services/user';

/**
* SamlAppAction defines Saml application edit and delete actions.
*/
export interface SamlAppAction {
/**
* actions controls Saml menu button view and edit and delete onClick behaviour.
*/
actions: {
/**
* showActions dictates whether to show or hide the Saml menu button.
*/
showActions: boolean;
/**
* startEdit triggers Saml app edit flow.
*/
startEdit: (resourceSpec: ResourceSpec) => void;
/**
* startDelete triggers Saml app delete flow.
*/
startDelete: (resourceSpec: ResourceSpec) => void;
};
/**
* currentAction specifies edit or delete mode.
*/
currentAction?: SamlAppActionMode;
/**
* deleteSamlAppAttempt is an attempt to delete Saml
* app in the backend.
*/
deleteSamlAppAttempt?: Attempt<void>;
/**
* samlAppToDelete defines Saml app item that is to be
* deleted from the unified view.
*/
samlAppToDelete?: SamlAppToDelete;
/**
* fetchSamlResourceAttempt is an attempt to fetch
* Saml resource spec from the backend. It is used to
* pre-populate input fields in the Saml Discover flow.
*/
fetchSamlResourceAttempt?: Attempt<SamlMeta>;
/**
* resourceSpec holds current Saml app resource spec.
*/
resourceSpec?: ResourceSpec;
/**
* userSamlIdPPerm holds user's RBAC permissions to
* saml_idp_service_provider resource.
*/
userSamlIdPPerm?: Access;
/**
* clearAction clears edit or delete flow.
*/
clearAction?: () => void;
/**
* onDelete handles Saml app delete in the backend.
*/
onDelete?: () => void;
}

export const SamlAppActionContext = createContext<SamlAppAction>(null);

export function useSamlAppAction() {
return useContext(SamlAppActionContext);
}

/**
* SamlAppActionProvider is a dummy provider to satisfy
* SamlAppActionContext in Teleport community edition.
*/
export function SamlAppActionProvider({
children,
}: {
children: React.ReactNode;
}) {
const value: SamlAppAction = {
actions: {
showActions: false,
startEdit: null,
startDelete: null,
},
};

return (
<SamlAppActionContext.Provider value={value}>
{children}
</SamlAppActionContext.Provider>
);
}

export type SamlAppActionMode = 'edit' | 'delete';
48 changes: 27 additions & 21 deletions web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React, { useState, Dispatch, SetStateAction } from 'react';
import React, { useState } from 'react';
import { ButtonBorder, ButtonWithMenu, MenuItem } from 'design';
import { LoginItem, MenuLogin } from 'shared/components/MenuLogin';
import { AwsLaunchButton } from 'shared/components/AwsLaunchButton';

import { UnifiedResource } from 'teleport/services/agents';
import cfg from 'teleport/config';

import useTeleport from 'teleport/useTeleport';
import { Database } from 'teleport/services/databases';
import { openNewTab } from 'teleport/lib/util';
Expand All @@ -34,24 +33,22 @@ import KubeConnectDialog from 'teleport/Kubes/ConnectDialog';
import useStickyClusterId from 'teleport/useStickyClusterId';
import { Node, sortNodeLogins } from 'teleport/services/nodes';
import { App } from 'teleport/services/apps';

import { ResourceKind } from 'teleport/Discover/Shared';

import { DiscoverEventResource } from 'teleport/services/userEvent';
import { useSamlAppAction } from 'teleport/SamlApplications/useSamlAppActions';

import type { ResourceSpec } from 'teleport/Discover/SelectResource/types';

type Props = {
resource: UnifiedResource;
setResourceSpec?: Dispatch<SetStateAction<ResourceSpec>>;
};

export const ResourceActionButton = ({ resource, setResourceSpec }: Props) => {
export const ResourceActionButton = ({ resource }: Props) => {
switch (resource.kind) {
case 'node':
return <NodeConnect node={resource} />;
case 'app':
return <AppLaunch app={resource} setResourceSpec={setResourceSpec} />;
return <AppLaunch app={resource} />;
case 'db':
return <DatabaseConnect database={resource} />;
case 'kube_cluster':
Expand Down Expand Up @@ -145,9 +142,8 @@ const DesktopConnect = ({ desktop }: { desktop: Desktop }) => {

type AppLaunchProps = {
app: App;
setResourceSpec?: Dispatch<SetStateAction<ResourceSpec>>;
};
const AppLaunch = ({ app, setResourceSpec }: AppLaunchProps) => {
const AppLaunch = ({ app }: AppLaunchProps) => {
const {
name,
launchUrl,
Expand All @@ -161,6 +157,7 @@ const AppLaunch = ({ app, setResourceSpec }: AppLaunchProps) => {
samlAppSsoUrl,
samlAppPreset,
} = app;
const { actions, userSamlIdPPerm } = useSamlAppAction();
if (awsConsole) {
return (
<AwsLaunchButton
Expand Down Expand Up @@ -190,18 +187,16 @@ const AppLaunch = ({ app, setResourceSpec }: AppLaunchProps) => {
</ButtonBorder>
);
}
function handleSamlAppEditButtonClick() {
setResourceSpec({
name: name,
event: DiscoverEventResource.SamlApplication,
kind: ResourceKind.SamlApplication,
samlMeta: { preset: samlAppPreset },
icon: 'application',
keywords: 'saml',
});
}
if (samlApp) {
if (setResourceSpec) {
if (actions.showActions) {
const currentSamlAppSpec: ResourceSpec = {
name: name,
event: DiscoverEventResource.SamlApplication,
kind: ResourceKind.SamlApplication,
samlMeta: { preset: samlAppPreset },
icon: 'application',
keywords: 'saml',
};
return (
<ButtonWithMenu
text="Log In"
Expand All @@ -214,7 +209,18 @@ const AppLaunch = ({ app, setResourceSpec }: AppLaunchProps) => {
forwardedAs="a"
title="Log in to SAML application"
>
<MenuItem onClick={handleSamlAppEditButtonClick}>Edit</MenuItem>
<MenuItem
onClick={() => actions.startEdit(currentSamlAppSpec)}
disabled={!userSamlIdPPerm.edit} // disable props does not disable onClick
>
Edit
</MenuItem>
<MenuItem
onClick={() => actions.startDelete(currentSamlAppSpec)}
disabled={!userSamlIdPPerm.remove} // disable props does not disable onClick
>
Delete
</MenuItem>
</ButtonWithMenu>
);
} else {
Expand Down
Loading

0 comments on commit 0618056

Please sign in to comment.