Skip to content

Commit f63ce00

Browse files
authored
chore: portal ui small fixes & enhancements (#497)
* chore: support copy destination field * chore: show title for nonsensitive credentials * fix: destination create flow bottom spacing * fix: skip topic step if no configured topics * refactor: destination creation form behavior depending on topics
1 parent 8afd7b8 commit f63ce00

File tree

3 files changed

+170
-126
lines changed

3 files changed

+170
-126
lines changed

internal/portal/src/scenes/CreateDestination/CreateDestination.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@
5757
&__step {
5858
display: flex;
5959
flex-direction: column;
60-
height: calc(100vh - var(--base-grid-multiplier) * 18);
61-
padding-bottom: var(--spacing-8);
60+
padding-bottom: var(--spacing-16);
6261
box-sizing: border-box;
6362

6463
h1 {
@@ -73,7 +72,6 @@
7372
display: flex;
7473
flex-direction: column;
7574
flex: 1;
76-
margin-bottom: var(--spacing-16);
7775
}
7876

7977
&__fields {

internal/portal/src/scenes/CreateDestination/CreateDestination.tsx

Lines changed: 144 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -11,130 +11,148 @@ import TopicPicker from "../../common/TopicPicker/TopicPicker";
1111
import { DestinationTypeReference } from "../../typings/Destination";
1212
import DestinationConfigFields from "../../common/DestinationConfigFields/DestinationConfigFields";
1313
import { getFormValues } from "../../utils/formHelper";
14+
import CONFIGS from "../../config";
1415

15-
const steps = [
16-
{
17-
title: "Select event topics",
18-
sidebar_shortname: "Event topics",
19-
description: "Select the event topics you want to send to your destination",
20-
isValid: (values: Record<string, any>) => {
21-
if (values.topics?.length > 0) {
22-
return true;
23-
}
24-
return false;
25-
},
26-
FormFields: ({
27-
defaultValue,
28-
onChange,
29-
}: {
30-
defaultValue: Record<string, any>;
31-
onChange: (value: Record<string, any>) => void;
32-
}) => {
33-
const [selectedTopics, setSelectedTopics] = useState<string[]>(
34-
defaultValue.topics ? defaultValue.topics.split(",") : []
35-
);
16+
type Step = {
17+
title: string;
18+
sidebar_shortname: string;
19+
description: string;
20+
isValid: (values: Record<string, any>) => boolean;
21+
FormFields: (props: {
22+
defaultValue: Record<string, any>;
23+
onChange: (value: Record<string, any>) => void;
24+
}) => React.ReactNode;
25+
action: string;
26+
};
27+
28+
const EVENT_TOPICS_STEP: Step = {
29+
title: "Select event topics",
30+
sidebar_shortname: "Event topics",
31+
description: "Select the event topics you want to send to your destination",
32+
isValid: (values: Record<string, any>) => {
33+
if (values.topics?.length > 0) {
34+
return true;
35+
}
36+
return false;
37+
},
38+
FormFields: ({
39+
defaultValue,
40+
onChange,
41+
}: {
42+
defaultValue: Record<string, any>;
43+
onChange: (value: Record<string, any>) => void;
44+
}) => {
45+
const [selectedTopics, setSelectedTopics] = useState<string[]>(
46+
defaultValue.topics ? defaultValue.topics.split(",") : []
47+
);
3648

37-
useEffect(() => {
38-
onChange({ topics: selectedTopics });
39-
}, [selectedTopics]);
49+
useEffect(() => {
50+
onChange({ topics: selectedTopics });
51+
}, [selectedTopics]);
4052

41-
return (
42-
<>
43-
<TopicPicker
44-
selectedTopics={selectedTopics}
45-
onTopicsChange={setSelectedTopics}
46-
/>
53+
return (
54+
<>
55+
<TopicPicker
56+
selectedTopics={selectedTopics}
57+
onTopicsChange={setSelectedTopics}
58+
/>
59+
<input
60+
readOnly
61+
type="text"
62+
name="topics"
63+
hidden
64+
required
65+
value={selectedTopics.length > 0 ? selectedTopics.join(",") : ""}
66+
/>
67+
</>
68+
);
69+
},
70+
action: "Next",
71+
};
72+
73+
const DESTINATION_TYPE_STEP: Step = {
74+
title: "Select destination type",
75+
sidebar_shortname: "Destination type",
76+
description:
77+
"Select the destination type you want to send to your destination",
78+
isValid: (values: Record<string, any>) => {
79+
if (!values.type) {
80+
return false;
81+
}
82+
return true;
83+
},
84+
FormFields: ({
85+
destinations,
86+
defaultValue,
87+
}: {
88+
destinations: DestinationTypeReference[];
89+
defaultValue: Record<string, any>;
90+
}) => (
91+
<div className="destination-types">
92+
{destinations?.map((destination) => (
93+
<label key={destination.type} className="destination-type-card">
4794
<input
48-
readOnly
49-
type="text"
50-
name="topics"
51-
hidden
95+
type="radio"
96+
name="type"
97+
value={destination.type}
5298
required
53-
value={selectedTopics.length > 0 ? selectedTopics.join(",") : ""}
99+
className="destination-type-radio"
100+
defaultChecked={
101+
defaultValue ? defaultValue.type === destination.type : undefined
102+
}
54103
/>
55-
</>
56-
);
57-
},
58-
action: "Next",
59-
},
60-
{
61-
title: "Select destination type",
62-
sidebar_shortname: "Destination type",
63-
description:
64-
"Select the destination type you want to send to your destination",
65-
isValid: (values: Record<string, any>) => {
66-
if (!values.type) {
67-
return false;
68-
}
69-
return true;
70-
},
71-
FormFields: ({
72-
destinations,
73-
defaultValue,
74-
}: {
75-
destinations: DestinationTypeReference[];
76-
defaultValue: Record<string, any>;
77-
}) => (
78-
<div className="destination-types">
79-
{destinations?.map((destination) => (
80-
<label key={destination.type} className="destination-type-card">
81-
<input
82-
type="radio"
83-
name="type"
84-
value={destination.type}
85-
required
86-
className="destination-type-radio"
87-
defaultChecked={
88-
defaultValue
89-
? defaultValue.type === destination.type
90-
: undefined
91-
}
92-
/>
93-
<div className="destination-type-content">
94-
<h3 className="subtitle-l">
95-
<span
96-
className="destination-type-content__icon"
97-
dangerouslySetInnerHTML={{ __html: destination.icon }}
98-
/>{" "}
99-
{destination.label}
100-
</h3>
101-
<p className="body-m muted">{destination.description}</p>
102-
</div>
103-
</label>
104-
))}
105-
</div>
106-
),
107-
action: "Next",
108-
},
109-
{
110-
title: "Configure destination",
111-
sidebar_shortname: "Configure destination",
112-
description:
113-
"Configure the destination you want to send to your destination",
114-
FormFields: ({
115-
defaultValue,
116-
destinations,
117-
}: {
118-
defaultValue: Record<string, any>;
119-
destinations: DestinationTypeReference[];
120-
}) => {
121-
const destinationType = destinations?.find(
122-
(d) => d.type === defaultValue.type
123-
);
124-
return (
125-
<DestinationConfigFields
126-
type={destinationType!}
127-
destination={undefined}
128-
/>
129-
);
130-
},
131-
action: "Create Destination",
104+
<div className="destination-type-content">
105+
<h3 className="subtitle-l">
106+
<span
107+
className="destination-type-content__icon"
108+
dangerouslySetInnerHTML={{ __html: destination.icon }}
109+
/>{" "}
110+
{destination.label}
111+
</h3>
112+
<p className="body-m muted">{destination.description}</p>
113+
</div>
114+
</label>
115+
))}
116+
</div>
117+
),
118+
action: "Next",
119+
};
120+
121+
const CONFIGURATION_STEP: Step = {
122+
title: "Configure destination",
123+
sidebar_shortname: "Configure destination",
124+
description: "Configure the destination you want to send to your destination",
125+
FormFields: ({
126+
defaultValue,
127+
destinations,
128+
}: {
129+
defaultValue: Record<string, any>;
130+
destinations: DestinationTypeReference[];
131+
}) => {
132+
const destinationType = destinations?.find(
133+
(d) => d.type === defaultValue.type
134+
);
135+
return (
136+
<DestinationConfigFields
137+
type={destinationType!}
138+
destination={undefined}
139+
/>
140+
);
132141
},
133-
];
142+
action: "Create Destination",
143+
};
134144

135145
export default function CreateDestination() {
136146
const apiClient = useContext(ApiContext);
137147

148+
const AVAILABLE_TOPICS = CONFIGS.TOPICS.split(",").filter(Boolean);
149+
let steps = [EVENT_TOPICS_STEP, DESTINATION_TYPE_STEP, CONFIGURATION_STEP];
150+
151+
// If there are no topics, skip the first step
152+
if (AVAILABLE_TOPICS.length === 0 && steps.length === 3) {
153+
steps = [DESTINATION_TYPE_STEP, CONFIGURATION_STEP];
154+
}
155+
138156
const navigate = useNavigate();
139157
const [currentStepIndex, setCurrentStepIndex] = useState(0);
140158
const [stepValues, setStepValues] = useState<Record<string, any>>({});
@@ -151,12 +169,24 @@ export default function CreateDestination() {
151169

152170
const destination_type = destinations?.find((d) => d.type === values.type);
153171

172+
let topics: string[];
173+
if (typeof values.topics === "string") {
174+
topics = values.topics.split(",").filter(Boolean);
175+
} else if (typeof values.topics === "undefined") {
176+
topics = ["*"];
177+
} else if (Array.isArray(values.topics)) {
178+
topics = values.topics;
179+
} else {
180+
// Default to all topics
181+
topics = ["*"];
182+
}
183+
154184
apiClient
155185
.fetch(`destinations`, {
156186
method: "POST",
157187
body: JSON.stringify({
158188
type: values.type,
159-
topics: values.topics.split(","),
189+
topics: topics,
160190
config: Object.fromEntries(
161191
Object.entries(values).filter(([key]) =>
162192
destination_type?.config_fields.some((field) => field.key === key)

internal/portal/src/scenes/Destination/Destination.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,21 @@ function DestinationDetailsField(props: {
246246
value: JSX.Element | string;
247247
}) {
248248
let label = "";
249+
let isSensitive = false;
250+
let shouldCopy = false;
249251
if (props.fieldType === "config") {
250-
label =
251-
props.type.config_fields.find((field) => field.key === props.fieldKey)
252-
?.label || "";
252+
const field = props.type.config_fields.find(
253+
(field) => field.key === props.fieldKey
254+
);
255+
label = field?.label || "";
256+
shouldCopy = field?.type === "text";
253257
} else {
254-
label =
255-
props.type.credential_fields.find((field) => field.key === props.fieldKey)
256-
?.label || "";
258+
const field = props.type.credential_fields.find(
259+
(field) => field.key === props.fieldKey
260+
);
261+
label = field?.label || "";
262+
shouldCopy = field?.type === "text" && !field?.sensitive;
263+
isSensitive = Boolean(field?.sensitive);
257264
}
258265
if (label === "") {
259266
label = props.fieldKey
@@ -269,10 +276,19 @@ function DestinationDetailsField(props: {
269276
return (
270277
<li>
271278
<span className="body-m">{label}</span>
272-
<span className="mono-s" title={typeof props.value === "string" && props.fieldType !== "credentials" ? props.value : undefined}>
273-
{typeof props.value === "string" && props.value.length > TRUNCATION_LENGTH
279+
<span
280+
className="mono-s"
281+
title={
282+
typeof props.value === "string" && !isSensitive
283+
? props.value
284+
: undefined
285+
}
286+
>
287+
{typeof props.value === "string" &&
288+
props.value.length > TRUNCATION_LENGTH
274289
? `${props.value.substring(0, TRUNCATION_LENGTH)}...`
275-
: props.value}
290+
: props.value}{" "}
291+
{shouldCopy && <CopyButton value={String(props.value)} />}
276292
</span>
277293
</li>
278294
);

0 commit comments

Comments
 (0)