diff --git a/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.story.tsx b/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.story.tsx
index ea05565de2150..a8b95d64ba316 100644
--- a/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.story.tsx
+++ b/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.story.tsx
@@ -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,
@@ -102,56 +103,51 @@ const ActionButton = Action;
export const Cards: Story = {
render() {
return (
-
- {[
- ...apps.map(resource =>
- makeUnifiedResourceViewItemApp(resource, {
- ActionButton: (
-
- 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) => (
- {}}
- selectResource={() => {}}
- selected={false}
- pinningSupport={PinningSupport.Supported}
- name={res.name}
- primaryIconName={res.primaryIconName}
- SecondaryIcon={res.SecondaryIcon}
- cardViewProps={res.cardViewProps}
- labels={res.labels}
- ActionButton={res.ActionButton}
- />
- ))}
-
+
+
+ {[
+ ...apps.map(resource =>
+ makeUnifiedResourceViewItemApp(resource, {
+ ActionButton: ,
+ })
+ ),
+ ...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) => (
+ {}}
+ selectResource={() => {}}
+ selected={false}
+ pinningSupport={PinningSupport.Supported}
+ name={res.name}
+ primaryIconName={res.primaryIconName}
+ SecondaryIcon={res.SecondaryIcon}
+ cardViewProps={res.cardViewProps}
+ labels={res.labels}
+ ActionButton={res.ActionButton}
+ />
+ ))}
+
+
);
},
};
diff --git a/web/packages/teleport/src/Main/Main.tsx b/web/packages/teleport/src/Main/Main.tsx
index a0105ea9cfe7c..bfaec119415b1 100644
--- a/web/packages/teleport/src/Main/Main.tsx
+++ b/web/packages/teleport/src/Main/Main.tsx
@@ -327,7 +327,7 @@ export const useNoMinWidth = () => {
}, []);
};
-const ContentMinWidth = ({ children }: { children: ReactNode }) => {
+export const ContentMinWidth = ({ children }: { children: ReactNode }) => {
const [enforceMinWidth, setEnforceMinWidth] = useState(true);
return (
diff --git a/web/packages/teleport/src/SamlApplications/useSamlAppActions.tsx b/web/packages/teleport/src/SamlApplications/useSamlAppActions.tsx
new file mode 100644
index 0000000000000..929f4ff149e78
--- /dev/null
+++ b/web/packages/teleport/src/SamlApplications/useSamlAppActions.tsx
@@ -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 .
+ */
+
+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;
+ /**
+ * 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;
+ /**
+ * 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(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 (
+
+ {children}
+
+ );
+}
+
+export type SamlAppActionMode = 'edit' | 'delete';
diff --git a/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx b/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx
index ce9e23e6e0008..05fcfe3de05ce 100644
--- a/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx
+++ b/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx
@@ -16,14 +16,13 @@
* along with this program. If not, see .
*/
-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';
@@ -34,23 +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>;
};
-export const ResourceActionButton = ({ resource, setResourceSpec }: Props) => {
+export const ResourceActionButton = ({ resource }: Props) => {
switch (resource.kind) {
case 'node':
return ;
case 'app':
- return ;
+ return ;
case 'db':
return ;
case 'kube_cluster':
@@ -144,9 +142,8 @@ const DesktopConnect = ({ desktop }: { desktop: Desktop }) => {
type AppLaunchProps = {
app: App;
- setResourceSpec?: Dispatch>;
};
-const AppLaunch = ({ app, setResourceSpec }: AppLaunchProps) => {
+const AppLaunch = ({ app }: AppLaunchProps) => {
const {
name,
launchUrl,
@@ -160,6 +157,7 @@ const AppLaunch = ({ app, setResourceSpec }: AppLaunchProps) => {
samlAppSsoUrl,
samlAppPreset,
} = app;
+ const { actions, userSamlIdPPerm } = useSamlAppAction();
if (awsConsole) {
return (
{
);
}
- 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 (
{
forwardedAs="a"
title="Log in to SAML application"
>
-
+
+
);
} else {
diff --git a/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx b/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx
index c7fff86888bb3..df2215f13a40d 100644
--- a/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx
+++ b/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx
@@ -16,8 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useCallback, useState } from 'react';
-
+import React, { useCallback, useState, useMemo } from 'react';
import { Flex } from 'design';
import { Danger } from 'design/Alert';
@@ -50,6 +49,10 @@ import { encodeUrlQueryParams } from 'teleport/components/hooks/useUrlFiltering'
import Empty, { EmptyStateInfo } from 'teleport/components/Empty';
import { FeatureFlags } from 'teleport/types';
import { UnifiedResource } from 'teleport/services/agents';
+import {
+ useSamlAppAction,
+ SamlAppActionProvider,
+} from 'teleport/SamlApplications/useSamlAppActions';
import { ResourceActionButton } from './ResourceActionButton';
import SearchPanel from './SearchPanel';
@@ -59,11 +62,13 @@ export function UnifiedResources() {
return (
-
+
+
+
);
}
@@ -149,7 +154,12 @@ export function ClusterResources({
getClusterPinnedResources: getCurrentClusterPinnedResources,
};
- const { fetch, resources, attempt, clear } = useUnifiedResourcesFetch({
+ const {
+ fetch,
+ resources: unfilteredResources,
+ attempt,
+ clear,
+ } = useUnifiedResourcesFetch({
fetchFunc: useCallback(
async (paginationParams, signal) => {
const response = await teleCtx.resourceService.fetchUnifiedResources(
@@ -187,6 +197,22 @@ export function ClusterResources({
),
});
+ const { samlAppToDelete } = useSamlAppAction();
+ const resources = useMemo(
+ () =>
+ samlAppToDelete?.backendDeleted
+ ? unfilteredResources.filter(
+ res =>
+ !(
+ res.kind === 'app' &&
+ res.samlApp &&
+ res.name === samlAppToDelete.name
+ )
+ )
+ : unfilteredResources,
+ [samlAppToDelete, unfilteredResources]
+ );
+
// This state is used to recognize when the `params` value has changed,
// and reset the overall state of `useUnifiedResourcesFetch` hook. It's tempting to use a
// `useEffect` here, but doing so can cause unwanted behavior where the previous,
diff --git a/web/packages/teleport/src/services/samlidp/types.ts b/web/packages/teleport/src/services/samlidp/types.ts
index 12add6ec1ed78..2270fed85ed43 100644
--- a/web/packages/teleport/src/services/samlidp/types.ts
+++ b/web/packages/teleport/src/services/samlidp/types.ts
@@ -75,3 +75,21 @@ export type SamlGcpWorkforce = {
poolName: string;
poolProviderName: string;
};
+
+/**
+ * SamlAppToDelete is used to define the name of an
+ * SAML app item to be deleted and its deletion state in the
+ * backend. Intended to be used in the unified resource view.
+ */
+export type SamlAppToDelete = {
+ /**
+ * name is the name of Saml app item to delete.
+ */
+ name: string;
+ // kind: string;
+ /**
+ * backendDeleted specifies if the item is deleted
+ * in the backend.
+ */
+ backendDeleted: boolean;
+};