From 678a6bf955d86d08cb0ee9eead7a4599dd5395bd Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Fri, 14 Apr 2023 00:57:44 -0700 Subject: [PATCH 1/6] Fix malformed JSON error response for 200 --- lib/web/integrations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/integrations.go b/lib/web/integrations.go index 89a550b91b515..2bc3af9ea2e04 100644 --- a/lib/web/integrations.go +++ b/lib/web/integrations.go @@ -133,7 +133,7 @@ func (h *Handler) integrationsDelete(w http.ResponseWriter, r *http.Request, p h return nil, trace.Wrap(err) } - return nil, nil + return OK(), nil } // integrationsGet returns an Integration based on its name From 0358583a1f34e76aac21e4c798c199faf058a9ad Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Fri, 14 Apr 2023 00:58:15 -0700 Subject: [PATCH 2/6] Fix making response when fetching integrations --- web/packages/teleport/src/config.ts | 2 +- .../src/services/integrations/integrations.ts | 11 +++++++++-- .../teleport/src/services/integrations/types.ts | 5 +++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index c4939a60aef1c..8427d376442cf 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -618,7 +618,7 @@ const cfg = { getIntegrationsUrl(integrationName?: string) { // Currently you can only create integrations at the root cluster. const clusterId = cfg.proxyCluster; - return generateResourcePath(cfg.api.integrationsPath, { + return generatePath(cfg.api.integrationsPath, { clusterId, name: integrationName, }); diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index 5ae480253d070..bcdce0c774b22 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -21,6 +21,7 @@ import { Integration, IntegrationCreateRequest, IntegrationStatusCode, + IntegrationListResponse, } from './types'; export const integrationService = { @@ -28,8 +29,14 @@ export const integrationService = { return api.get(cfg.getIntegrationsUrl(name)).then(makeIntegration); }, - fetchIntegrations(): Promise { - return api.get(cfg.getIntegrationsUrl()).then(makeIntegrations); + fetchIntegrations(): Promise { + return api.get(cfg.getIntegrationsUrl()).then(resp => { + const integrations = resp.items ?? []; + return { + items: integrations.map(makeIntegration), + nextKey: resp?.nextKey, + }; + }); }, createIntegration(req: IntegrationCreateRequest): Promise { diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 8fcf64b56865e..9a6bb57e4ce06 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -97,3 +97,8 @@ export type IntegrationCreateRequest = { subKind: IntegrationKind; awsoidc?: IntegrationSpecAwsOidc; }; + +export type IntegrationListResponse = { + items: Integration[]; + nextKey?: string; +}; From 653acde99932ba7407827dad1cbda77d087e6499 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Fri, 14 Apr 2023 01:03:20 -0700 Subject: [PATCH 3/6] Create re-usable integration ops hook (only delete for now) --- .../Integrations/useIntegrationOperation.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 web/packages/teleport/src/Integrations/useIntegrationOperation.ts diff --git a/web/packages/teleport/src/Integrations/useIntegrationOperation.ts b/web/packages/teleport/src/Integrations/useIntegrationOperation.ts new file mode 100644 index 0000000000000..b96b80336e594 --- /dev/null +++ b/web/packages/teleport/src/Integrations/useIntegrationOperation.ts @@ -0,0 +1,50 @@ +/** + * 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 { useState } from 'react'; + +import { integrationService } from 'teleport/services/integrations'; + +import type { Integration, Plugin } from 'teleport/services/integrations'; + +export function useIntegrationOperation() { + const [operation, setOperation] = useState({ + type: 'none', + } as Operation); + + function clear() { + setOperation({ type: 'none' }); + } + + function remove() { + return integrationService.deleteIntegration(operation.item.name); + } + + function onRemove(item: Integration) { + setOperation({ type: 'delete', item }); + } + + return { + ...operation, + clear, + remove, + onRemove, + }; +} + +export type Operation = { + type: 'create' | 'edit' | 'delete' | 'reset' | 'none'; + item?: Plugin | Integration; +}; From a0b73372f169c1825221128a1417d8797eaf535f Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Fri, 14 Apr 2023 01:04:29 -0700 Subject: [PATCH 4/6] Create delete dialog --- .../Integrations/DeleteIntegrationDialog.tsx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 web/packages/teleport/src/Integrations/DeleteIntegrationDialog.tsx diff --git a/web/packages/teleport/src/Integrations/DeleteIntegrationDialog.tsx b/web/packages/teleport/src/Integrations/DeleteIntegrationDialog.tsx new file mode 100644 index 0000000000000..8376e13d6da72 --- /dev/null +++ b/web/packages/teleport/src/Integrations/DeleteIntegrationDialog.tsx @@ -0,0 +1,67 @@ +/** + * 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 { ButtonSecondary, ButtonWarning, Text, Alert } from 'design'; +import Dialog, { + DialogHeader, + DialogTitle, + DialogContent, + DialogFooter, +} from 'design/DialogConfirmation'; +import useAttempt from 'shared/hooks/useAttemptNext'; + +type Props = { + onClose(): void; + onDelete(): Promise; + name: string; +}; + +export function DeleteIntegrationDialog(props: Props) { + const { onClose, onDelete } = props; + const { attempt, run } = useAttempt(); + const isDisabled = attempt.status === 'processing'; + + function onOk() { + run(() => onDelete()); + } + + return ( + + + Delete Integration? + + + {attempt.status === 'failed' && } + + Are you sure you want to delete integration{' '} + + {props.name} + {' '} + ? + + + + + Yes, Remove Integration + + + Cancel + + + + ); +} From 54996aed20ef3016951954a4f7190db3e3d443eb Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Fri, 14 Apr 2023 01:05:22 -0700 Subject: [PATCH 5/6] Allow deleting integrations --- .../src/Integrations/IntegrationList.tsx | 47 ++++++++------- .../src/Integrations/Integrations.story.tsx | 10 +++- .../src/Integrations/Integrations.tsx | 60 ++++++++++++++----- 3 files changed, 78 insertions(+), 39 deletions(-) diff --git a/web/packages/teleport/src/Integrations/IntegrationList.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx index 70212e761bcff..db28ab36b9a26 100644 --- a/web/packages/teleport/src/Integrations/IntegrationList.tsx +++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx @@ -35,7 +35,8 @@ import { type Props = { list: IntegrationLike[]; - onDelete(i: IntegrationLike): void; + onDeletePlugin?(p: Plugin): void; + onDeleteIntegration?(i: Integration): void; }; type IntegrationLike = Integration | Plugin; @@ -69,11 +70,29 @@ export function IntegrationList(props: Props) { }, { altKey: 'options-btn', - render: item => ( - props.onDelete(item) : null} - /> - ), + render: item => { + if (item.resourceType === 'plugin') { + return ( + + + props.onDeletePlugin(item)}> + Delete... + + + + ); + } + + return ( + + + props.onDeleteIntegration(item)}> + Delete... + + + + ); + }, }, ]} emptyText="No Results Found" @@ -100,20 +119,6 @@ const StatusCell = ({ item }: { item: IntegrationLike }) => { ); }; -const ActionCell = ({ onDelete }: { onDelete: () => void }) => { - if (!onDelete) { - return null; - } - - return ( - - - Delete... - - - ); -}; - enum Status { Success, Warning, @@ -170,7 +175,7 @@ const IconCell = ({ item }: { item: IntegrationLike }) => { // Default is integration. switch (item.kind) { case IntegrationKind.AwsOidc: - formattedText = 'Amazon Web Services (OIDC)'; + formattedText = item.name; icon = ; break; } diff --git a/web/packages/teleport/src/Integrations/Integrations.story.tsx b/web/packages/teleport/src/Integrations/Integrations.story.tsx index a20117b5448e7..b63a10d71d740 100644 --- a/web/packages/teleport/src/Integrations/Integrations.story.tsx +++ b/web/packages/teleport/src/Integrations/Integrations.story.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { IntegrationList } from './IntegrationList'; +import { DeleteIntegrationDialog } from './DeleteIntegrationDialog'; import { plugins, integrations } from './fixtures'; export default { @@ -24,10 +25,15 @@ export default { }; export function List() { + return ; +} + +export function DeleteDialog() { return ( - null} onDelete={() => null} + name="some-integration-name" /> ); } diff --git a/web/packages/teleport/src/Integrations/Integrations.tsx b/web/packages/teleport/src/Integrations/Integrations.tsx index 2cc8b8d7c1618..fb4b605fba0ec 100644 --- a/web/packages/teleport/src/Integrations/Integrations.tsx +++ b/web/packages/teleport/src/Integrations/Integrations.tsx @@ -28,10 +28,13 @@ import { integrationService } from 'teleport/services/integrations'; import { IntegrationsAddButton } from './IntegrationsAddButton'; import { IntegrationList } from './IntegrationList'; +import { DeleteIntegrationDialog } from './DeleteIntegrationDialog'; +import { useIntegrationOperation } from './useIntegrationOperation'; import type { Integration } from 'teleport/services/integrations'; export function Integrations() { + const integrationOps = useIntegrationOperation(); const [items, setItems] = useState([]); const { attempt, run } = useAttempt('processing'); @@ -39,25 +42,50 @@ export function Integrations() { const canCreateIntegrations = ctx.storeUser.getIntegrationsAccess().create; useEffect(() => { - run(() => integrationService.fetchIntegrations().then(setItems)); + // TODO(lisa): handle paginating as a follow up polish. + // Default fetch is 1k of integrations, which is plenty for beginning. + run(() => + integrationService.fetchIntegrations().then(resp => setItems(resp.items)) + ); }, []); + function deleteIntegration() { + return integrationOps.remove().then(() => { + const updatedItems = items.filter( + i => i.name !== integrationOps.item.name + ); + setItems(updatedItems); + integrationOps.clear(); + }); + } + return ( - - - Integrations - - - {attempt.status === 'failed' && } - {attempt.status === 'processing' && ( - - - - )} - {attempt.status === 'success' && ( - /* TODO(lisa): deletion is stubbed until backend is implemented*/ - + <> + + + Integrations + + + {attempt.status === 'failed' && } + {attempt.status === 'processing' && ( + + + + )} + {attempt.status === 'success' && ( + + )} + + {integrationOps.type === 'delete' && ( + )} - + ); } From 4455462f048c242a7e2a5a24174db6c95820b86c Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Fri, 14 Apr 2023 02:25:27 -0700 Subject: [PATCH 6/6] Fix lint --- .../Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx | 2 +- web/packages/teleport/src/services/integrations/integrations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx b/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx index 0f47d8e35b5f1..1964a2e464482 100644 --- a/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx +++ b/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx @@ -69,7 +69,7 @@ export function ConnectAwsAccount() { function fetchAwsIntegrations() { run(() => integrationService.fetchIntegrations().then(res => { - const options = res.map(i => { + const options = res.items.map(i => { if (i.kind === 'aws-oidc') { return { value: i.name, diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index bcdce0c774b22..b94111b43e425 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -31,7 +31,7 @@ export const integrationService = { fetchIntegrations(): Promise { return api.get(cfg.getIntegrationsUrl()).then(resp => { - const integrations = resp.items ?? []; + const integrations = resp?.items ?? []; return { items: integrations.map(makeIntegration), nextKey: resp?.nextKey,