Skip to content

Commit ab491ec

Browse files
Linker44Adrian Galvanlucanovera
authored
Custom integration UI updates (#7321)
Co-authored-by: Adrian Galvan <galvana@uci.edu> Co-authored-by: Lucano Vera <lucanovera@live.com.ar>
1 parent 49771dd commit ab491ec

File tree

11 files changed

+282
-50
lines changed

11 files changed

+282
-50
lines changed

changelog/7321.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type: Added
2+
description: Added custom integration UI indicators and deletion support
3+
pr: 7321
4+
labels: []

clients/admin-ui/src/features/connector-templates/connector-template.slice.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,17 @@ export const connectorTemplateApi = baseApi.injectEndpoints({
2929
},
3030
invalidatesTags: () => ["Connection Type"],
3131
}),
32+
deleteConnectorTemplate: build.mutation<void, string>({
33+
query: (connectorTemplateType) => ({
34+
url: `${CONNECTOR_TEMPLATE}/${connectorTemplateType}`,
35+
method: "DELETE",
36+
}),
37+
invalidatesTags: () => ["Connection Type"],
38+
}),
3239
}),
3340
});
3441

35-
export const { useRegisterConnectorTemplateMutation } = connectorTemplateApi;
42+
export const {
43+
useRegisterConnectorTemplateMutation,
44+
useDeleteConnectorTemplateMutation,
45+
} = connectorTemplateApi;

clients/admin-ui/src/features/datastore-connections/system_portal_config/ConnectionForm.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import React, { useEffect, useState } from "react";
1515
import { useAppSelector } from "~/app/hooks";
1616
import ConnectorTemplateUploadModal from "~/features/connector-templates/ConnectorTemplateUploadModal";
1717
import { ConnectorParameters } from "~/features/datastore-connections/system_portal_config/forms/ConnectorParameters";
18+
import { useRemoveCustomIntegration } from "~/features/integrations/hooks/useRemoveCustomIntegration";
1819
import {
1920
ConnectionConfigurationResponse,
2021
ConnectionSystemTypeMap,
@@ -75,6 +76,8 @@ const ConnectionForm = ({ connectionConfig, systemFidesKey }: Props) => {
7576
}, [data]);
7677

7778
const uploadTemplateModal = useDisclosure();
79+
const { handleRemove: handleRemoveCustomIntegration, modalContext } =
80+
useRemoveCustomIntegration(selectedConnectionOption);
7881

7982
/* STEPS TO UNIFY the database and saas forms
8083
7. Get it working for manual connectors
@@ -84,7 +87,7 @@ const ConnectionForm = ({ connectionConfig, systemFidesKey }: Props) => {
8487
return (
8588
<Box id="con-wrapper" px={6}>
8689
<Flex py={5}>
87-
<Stack direction={{ base: "column", lg: "row" }}>
90+
<Stack direction={{ base: "column", lg: "row" }} alignItems="center">
8891
<ConnectionListDropdown
8992
list={dropDownOptions}
9093
label="Integration type"
@@ -100,21 +103,35 @@ const ConnectionForm = ({ connectionConfig, systemFidesKey }: Props) => {
100103
) : null}
101104

102105
<Restrict scopes={[ScopeRegistryEnum.CONNECTOR_TEMPLATE_REGISTER]}>
103-
<Button
104-
htmlType="submit"
105-
data-testid="upload-btn"
106-
onClick={uploadTemplateModal.onOpen}
107-
className="ml-2"
108-
>
109-
Upload integration
110-
</Button>
106+
<Flex gap={2}>
107+
{connectionConfig &&
108+
selectedConnectionOption?.custom &&
109+
selectedConnectionOption?.default_connector_available && (
110+
<Button
111+
type="link"
112+
danger
113+
data-testid="delete-custom-integration-btn"
114+
onClick={handleRemoveCustomIntegration}
115+
>
116+
Remove
117+
</Button>
118+
)}
119+
<Button
120+
htmlType="submit"
121+
data-testid="upload-btn"
122+
onClick={uploadTemplateModal.onOpen}
123+
>
124+
Upload integration
125+
</Button>
126+
</Flex>
111127
</Restrict>
112128
</Stack>
113129

114130
<ConnectorTemplateUploadModal
115131
isOpen={uploadTemplateModal.isOpen}
116132
onClose={uploadTemplateModal.onClose}
117133
/>
134+
{modalContext}
118135
</Flex>
119136
{selectedConnectionOption?.type &&
120137
[

clients/admin-ui/src/features/datastore-connections/system_portal_config/ConnectionListDropdown.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {
1212
ChakraMenuItem as MenuItem,
1313
ChakraMenuList as MenuList,
1414
ChakraText as Text,
15+
Icons,
1516
SearchLineIcon,
1617
Tooltip,
1718
} from "fidesui";
18-
import { useCallback, useMemo, useRef, useState } from "react";
19+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1920

2021
import { useAppSelector } from "~/app/hooks";
2122
import {
@@ -137,6 +138,20 @@ export const useConnectionListDropDown = ({
137138
}
138139
}, [connectionConfig, connectionOptions]);
139140

141+
// Sync selectedValue with latest connectionOptions when data refreshes
142+
// This ensures the selected value is updated after operations like deleting a custom template
143+
useEffect(() => {
144+
if (selectedValue && connectionOptions.length > 0) {
145+
const updatedValue = connectionOptions.find(
146+
(ct) => ct.identifier === selectedValue.identifier,
147+
);
148+
// Only update if we found a match and it's a different object reference
149+
if (updatedValue && updatedValue !== selectedValue) {
150+
setSelectedValue(updatedValue);
151+
}
152+
}
153+
}, [connectionOptions, selectedValue]);
154+
140155
return { dropDownOptions, selectedValue, setSelectedValue, systemType };
141156
};
142157

@@ -213,9 +228,18 @@ const ConnectionListDropdown = ({
213228
data-testid="select-dropdown-btn"
214229
width="272px"
215230
>
216-
<Text noOfLines={1} style={{ wordBreak: "break-all" }}>
217-
{selectedText ?? label}
218-
</Text>
231+
<Flex alignItems="center" gap={1}>
232+
<Text noOfLines={1} style={{ wordBreak: "break-all" }}>
233+
{selectedText ?? label}
234+
</Text>
235+
{selectedValue?.custom && (
236+
<Tooltip title="Custom integration" placement="top">
237+
<Box as="span" display="inline-flex">
238+
<Icons.SettingsCheck size={16} />
239+
</Box>
240+
</Tooltip>
241+
)}
242+
</Flex>
219243
</MenuButton>
220244
{isOpen ? (
221245
<MenuList
@@ -294,6 +318,18 @@ const ConnectionListDropdown = ({
294318
>
295319
{key}
296320
</Text>
321+
{option.value.custom && (
322+
<Tooltip title="Custom integration" placement="top">
323+
<Box
324+
as="span"
325+
ml={2}
326+
display="inline-flex"
327+
cursor="pointer"
328+
>
329+
<Icons.SettingsCheck size={16} />
330+
</Box>
331+
</Tooltip>
332+
)}
297333
</MenuItem>
298334
</Tooltip>
299335
))}

clients/admin-ui/src/features/integrations/IntegrationBox.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
ChakraFlex as Flex,
55
ChakraText as Text,
66
ChakraWrap as Wrap,
7+
Icons,
78
Tag,
9+
Tooltip,
810
} from "fidesui";
911
import { ReactNode } from "react";
1012

@@ -59,14 +61,9 @@ const IntegrationBox = ({
5961
connectionTypes,
6062
);
6163

62-
// Only pass the saas type if it's a valid SaasConnectionTypes value
63-
const saasType = integration?.saas_config?.type;
64-
const isSaasType = (type: string): type is SaasConnectionTypes =>
65-
Object.values(SaasConnectionTypes).includes(type as SaasConnectionTypes);
66-
6764
const connectionOption = useIntegrationOption(
6865
integration?.connection_type,
69-
saasType && isSaasType(saasType) ? saasType : undefined,
66+
integration?.saas_config?.type as SaasConnectionTypes,
7067
);
7168

7269
const { handleAuthorize, needsAuthorization } = useIntegrationAuthorization({
@@ -88,9 +85,19 @@ const IntegrationBox = ({
8885
<Flex>
8986
<ConnectionTypeLogo data={logoData} size={50} />
9087
<Flex direction="column" flexGrow={1} marginLeft="16px">
91-
<Text color="gray.700" fontWeight="semibold">
92-
{integration?.name || "(No name)"}
93-
</Text>
88+
<Flex alignItems="center" gap={2}>
89+
<Text color="gray.700" fontWeight="semibold">
90+
{integration?.name || "(No name)"}
91+
</Text>
92+
{connectionOption?.custom && (
93+
<Tooltip title="Custom integration" placement="top">
94+
<Box as="span" display="inline-flex">
95+
<Icons.SettingsCheck size={16} />
96+
</Box>
97+
</Tooltip>
98+
)}
99+
{otherButtons}
100+
</Flex>
94101
{showTestNotice ? (
95102
<ConnectionStatusNotice
96103
testData={testData}
@@ -102,7 +109,7 @@ const IntegrationBox = ({
102109
</Text>
103110
)}
104111
</Flex>
105-
<div className="flex gap-4">
112+
<Flex marginLeft="auto" gap={4} flexShrink={0}>
106113
{showDeleteButton && integration && (
107114
<DeleteConnectionModal
108115
showMenu={false}
@@ -126,13 +133,12 @@ const IntegrationBox = ({
126133
Test connection
127134
</Button>
128135
)}
129-
{otherButtons}
130136
{onConfigureClick && (
131137
<Button onClick={onConfigureClick} data-testid="configure-btn">
132138
{configureButtonLabel}
133139
</Button>
134140
)}
135-
</div>
141+
</Flex>
136142
</Flex>
137143
<Wrap marginTop="16px">
138144
{typeInfo.tags.map((item: string) => (

clients/admin-ui/src/features/integrations/SelectableIntegrationBox.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
ChakraFlex as Flex,
55
ChakraText as Text,
66
ChakraWrap as Wrap,
7+
Icons,
78
Tag,
9+
Tooltip,
810
Typography,
911
} from "fidesui";
1012

@@ -14,6 +16,8 @@ import ConnectionTypeLogo from "~/features/datastore-connections/ConnectionTypeL
1416
import getIntegrationTypeInfo, {
1517
IntegrationTypeInfo,
1618
} from "~/features/integrations/add-integration/allIntegrationTypes";
19+
import { SaasConnectionTypes } from "~/features/integrations/types/SaasConnectionTypes";
20+
import useIntegrationOption from "~/features/integrations/useIntegrationOption";
1721
import { getCategoryLabel } from "~/features/integrations/utils/categoryUtils";
1822
import { ConnectionConfigurationResponse } from "~/types/api";
1923

@@ -43,6 +47,11 @@ const SelectableIntegrationBox = ({
4347
integration?.saas_config?.type,
4448
);
4549

50+
const connectionOption = useIntegrationOption(
51+
integration?.connection_type,
52+
integration?.saas_config?.type as SaasConnectionTypes,
53+
);
54+
4655
// Handle click outside to unfocus when selected
4756
const boxRef = useClickOutside<HTMLDivElement>(() => {
4857
if (selected && onUnfocus) {
@@ -79,18 +88,27 @@ const SelectableIntegrationBox = ({
7988
marginRight="12px"
8089
width={0}
8190
>
82-
<Typography.Text
83-
strong
84-
style={{
85-
color: "var(--chakra-colors-gray-700)",
86-
fontSize: "14px",
87-
}}
88-
ellipsis={{
89-
tooltip: integration?.name || "(No name)",
90-
}}
91-
>
92-
{integration?.name || "(No name)"}
93-
</Typography.Text>
91+
<Flex alignItems="center" gap={1}>
92+
<Typography.Text
93+
strong
94+
style={{
95+
color: "var(--chakra-colors-gray-700)",
96+
fontSize: "14px",
97+
}}
98+
ellipsis={{
99+
tooltip: integration?.name || "(No name)",
100+
}}
101+
>
102+
{integration?.name || "(No name)"}
103+
</Typography.Text>
104+
{connectionOption?.custom && (
105+
<Tooltip title="Custom integration" placement="top">
106+
<Box as="span" display="inline-flex">
107+
<Icons.SettingsCheck size={16} />
108+
</Box>
109+
</Tooltip>
110+
)}
111+
</Flex>
94112
<Text color="gray.600" fontSize="xs" mt={1}>
95113
{getCategoryLabel(typeInfo.category)}
96114
</Text>
Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,53 @@
1+
import { Button } from "fidesui";
2+
13
import { IntegrationTypeInfo } from "~/features/integrations/add-integration/allIntegrationTypes";
4+
import { useRemoveCustomIntegration } from "~/features/integrations/hooks/useRemoveCustomIntegration";
25
import IntegrationBox from "~/features/integrations/IntegrationBox";
6+
import { SaasConnectionTypes } from "~/features/integrations/types/SaasConnectionTypes";
7+
import useIntegrationOption from "~/features/integrations/useIntegrationOption";
38

49
const IntegrationTypeDetail = ({
510
integrationType,
611
onConfigure,
712
}: {
813
integrationType?: IntegrationTypeInfo;
914
onConfigure: () => void;
10-
}) => (
11-
<>
12-
<IntegrationBox
13-
integration={integrationType?.placeholder}
14-
integrationTypeInfo={integrationType}
15-
onConfigureClick={onConfigure}
16-
/>
17-
{integrationType?.overview}
18-
</>
19-
);
15+
}) => {
16+
const connectionOption = useIntegrationOption(
17+
integrationType?.placeholder.connection_type,
18+
integrationType?.placeholder.saas_config?.type as SaasConnectionTypes,
19+
);
20+
21+
const { handleRemove: handleRemoveCustomIntegration, modalContext } =
22+
useRemoveCustomIntegration(connectionOption);
23+
24+
const showRemoveButton =
25+
!!connectionOption?.custom &&
26+
!!connectionOption?.default_connector_available;
27+
28+
return (
29+
<>
30+
{modalContext}
31+
<IntegrationBox
32+
integration={integrationType?.placeholder}
33+
integrationTypeInfo={integrationType}
34+
onConfigureClick={onConfigure}
35+
otherButtons={
36+
showRemoveButton ? (
37+
<Button
38+
type="link"
39+
danger
40+
data-testid="remove-custom-integration-btn"
41+
onClick={handleRemoveCustomIntegration}
42+
>
43+
Remove
44+
</Button>
45+
) : undefined
46+
}
47+
/>
48+
{integrationType?.overview}
49+
</>
50+
);
51+
};
2052

2153
export default IntegrationTypeDetail;

0 commit comments

Comments
 (0)