Skip to content

Commit 7dd1f74

Browse files
authored
console: Streams added Strict toggle (#1123)
* console: Streams added Strict toggle * console: EventsBrowser: choose correct ingest type icon * console: improve Stream Api key editor, automatically save added write keys
1 parent f276c7c commit 7dd1f74

File tree

6 files changed

+173
-15
lines changed

6 files changed

+173
-15
lines changed

webapps/console/components/ApiKeyEditor/ApiKeyEditor.tsx

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { ApiKey } from "../../lib/schema";
22
import { useState } from "react";
33
import { Button, Table, Tooltip } from "antd";
44
import { branding } from "../../lib/branding";
5-
import { confirmOp, copyTextToClipboard } from "../../lib/ui";
5+
import { confirmOp, copyTextToClipboard, feedbackSuccess } from "../../lib/ui";
66
import { FaCopy, FaPlus, FaTrash } from "react-icons/fa";
77
import { randomId } from "juava";
8+
import { WidgetProps } from "@rjsf/utils";
9+
import { getConfigApi } from "../../lib/useApi";
10+
import { useWorkspace } from "../../lib/context";
11+
import { RefreshCw } from "lucide-react";
812
import { CustomWidgetProps } from "../ConfigObjectEditor/Editors";
913

1014
const CopyToClipboard: React.FC<{ text: string }> = ({ text }) => {
@@ -160,6 +164,134 @@ export const ApiKeysEditor: React.FC<CustomWidgetProps<ApiKey[]> & { compact?: b
160164
);
161165
};
162166

163-
export const BrowserKeysEditor: React.FC<CustomWidgetProps<ApiKey[]> & { compact?: boolean }> = (props: any) => {
164-
return <ApiKeysEditor {...props} compact={true} />;
167+
export const StreamKeysEditor: React.FC<WidgetProps<ApiKey[]>> = props => {
168+
const context = props.formContext;
169+
const workspace = useWorkspace();
170+
const type = context?.type;
171+
const [loading, setLoading] = useState(false);
172+
const [keys, setKeys] = useState<ApiKey[]>(props.value || []);
173+
const columns: any[] = [
174+
{
175+
title: "Key",
176+
key: "id",
177+
width: "90%",
178+
render: (key: ApiKey) => {
179+
return key.plaintext && key.id !== "Generating key" ? (
180+
<div className="flex items-center">
181+
<Tooltip
182+
className="cursor-pointer"
183+
title={
184+
<>
185+
{" "}
186+
<strong>Key generated!</strong> Copy this key and store it in a safe place. You will not be able to
187+
see it again.
188+
</>
189+
}
190+
>
191+
<code>
192+
{key.id}:{key.plaintext}
193+
</code>
194+
</Tooltip>
195+
<CopyToClipboard text={`${key.id}:${key.plaintext}`} />
196+
</div>
197+
) : (
198+
<>
199+
<Tooltip
200+
className="cursor-pointer"
201+
title={
202+
<>
203+
{branding.productName} doesn't store full version of a key. If you haven't recorded the key, generate
204+
a new one
205+
</>
206+
}
207+
>
208+
<code>
209+
{key.id}:{key?.hint?.replace("*", "*".repeat(32 - 6))}
210+
</code>
211+
</Tooltip>
212+
</>
213+
);
214+
},
215+
},
216+
];
217+
columns.push({
218+
title: "",
219+
className: "text-right",
220+
key: "actions",
221+
render: (key: ApiKey) => {
222+
if (key.id === "Generating key") {
223+
return <></>;
224+
}
225+
return (
226+
<div>
227+
<Button
228+
type="text"
229+
onClick={async () => {
230+
if (
231+
key.plaintext ||
232+
(await confirmOp("Are you sure you want to delete this API key? You won't be able to recover it"))
233+
) {
234+
const newVal = keys.filter(k => k.id !== key.id);
235+
setKeys(newVal);
236+
props.onChange(newVal);
237+
}
238+
}}
239+
>
240+
<FaTrash />
241+
</Button>
242+
</div>
243+
);
244+
},
245+
});
246+
return (
247+
<div className={"pt-3"}>
248+
{keys.length === 0 && !loading && <div className="flex text-textDisabled justify-center">Keys list is empty</div>}
249+
{(keys.length > 0 || loading) && (
250+
<Table
251+
size={"small"}
252+
showHeader={false}
253+
columns={columns}
254+
dataSource={loading ? [...keys, { id: "Generating key", hint: "..." }] : keys}
255+
pagination={false}
256+
rowKey={k => k.id}
257+
/>
258+
)}
259+
<div className="flex justify-between p-2">
260+
{keys.find(key => !!key.plaintext) ? (
261+
<div className="text-text text-sm">
262+
Congrats! You're generated a new key(s). Copy it an keep in the safe place.
263+
<br />
264+
You will not be able to see it again once you leave the page
265+
</div>
266+
) : (
267+
<div></div>
268+
)}
269+
<Button
270+
type="text"
271+
onClick={async () => {
272+
try {
273+
const newKey = randomId(32);
274+
const newVal = [...keys, { id: randomId(32), plaintext: newKey, hint: hint(newKey) }];
275+
if (type === "stream" && !context.isNew) {
276+
setLoading(true);
277+
await getConfigApi(workspace.id, "stream").update(context.object.id, {
278+
[props.name]: newVal,
279+
});
280+
setKeys(newVal);
281+
props.onChange(newVal);
282+
feedbackSuccess("Write Key Saved");
283+
} else {
284+
setKeys(newVal);
285+
props.onChange(newVal);
286+
}
287+
} finally {
288+
setLoading(false);
289+
}
290+
}}
291+
>
292+
{loading ? <RefreshCw className={"w-4 h-4 animate-spin"} /> : <FaPlus />}
293+
</Button>
294+
</div>
295+
</div>
296+
);
165297
};

webapps/console/components/ConfigObjectEditor/ConfigEditor.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ const FormList: React.FC<ObjectFieldTemplateProps> = props => {
189189
);
190190
};
191191

192-
const CustomCheckbox = function (props) {
192+
export const CustomCheckbox = function (props) {
193193
return <Switch checked={props.value} onClick={() => props.onChange(!props.value)} />;
194194
};
195195

@@ -247,7 +247,7 @@ const EditorComponent: React.FC<EditorComponentProps> = props => {
247247
const schema = zodToJsonSchema(objectTypeFactory(object));
248248
const [formState, setFormState] = useState<any | undefined>(undefined);
249249
const hasErrors = formState?.errors?.length > 0;
250-
const isTouched = formState !== undefined || !!createNew;
250+
const [isTouched, setTouched] = useState<boolean>(!!createNew);
251251
const [testResult, setTestResult] = useState<any>(undefined);
252252

253253
const uiSchema = getUiSchema(schema, fields);
@@ -257,6 +257,7 @@ const EditorComponent: React.FC<EditorComponentProps> = props => {
257257
const onFormChange = state => {
258258
setFormState(state);
259259
setTestResult(undefined);
260+
setTouched(true);
260261
log.atDebug().log(`Updating editor form state`, state);
261262
};
262263
const withLoading = (fn: () => Promise<void>) => async () => {
@@ -291,11 +292,11 @@ const EditorComponent: React.FC<EditorComponentProps> = props => {
291292
liveValidate={true}
292293
validator={validator}
293294
onSubmit={async ({ formData }) => {
294-
if (onTest && testConnectionEnabled && testConnectionEnabled(formData || object)) {
295+
if (onTest && (typeof testConnectionEnabled === "undefined" || testConnectionEnabled(formData || object))) {
295296
const testRes = testResult || (await onTest(formState?.formData || object));
296297
if (!testRes.ok) {
297298
modal.confirm({
298-
title: "Connection test failed",
299+
title: "Check failed",
299300
content: testRes.error,
300301
okText: "Save anyway",
301302
okType: "danger",

webapps/console/components/DataView/EventsBrowser.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1174,7 +1174,7 @@ const IncomingEventsTable = ({ loadEvents, loading, streamType, entityType, acto
11741174
//dataIndex: "type",
11751175
render: (d: IncomingEvent) => {
11761176
const eventName = d.type === "track" ? d.event?.event || d.type : d.type;
1177-
const isDeviceEvent = d.pagePath;
1177+
const isDeviceEvent = d.ingestType === "browser";
11781178
return (
11791179
<Tooltip title={eventName}>
11801180
<Tag

webapps/console/lib/schema/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const StreamConfig = ConfigEntityBase.merge(
125125
authorizedJavaScriptDomains: z.string().optional(),
126126
publicKeys: z.array(ApiKey).optional(),
127127
privateKeys: z.array(ApiKey).optional(),
128+
strict: z.boolean().optional(),
128129
})
129130
);
130131
export type StreamConfig = z.infer<typeof StreamConfig>;

webapps/console/pages/[workspaceId]/streams.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { WorkspacePageLayout } from "../../components/PageLayout/WorkspacePageLayout";
22
import { Button, Input, notification, Tag, Tooltip } from "antd";
3-
import { ConfigEditor, ConfigEditorProps } from "../../components/ConfigObjectEditor/ConfigEditor";
3+
import { ConfigEditor, ConfigEditorProps, CustomCheckbox } from "../../components/ConfigObjectEditor/ConfigEditor";
44
import { StreamConfig } from "../../lib/schema";
55
import { useAppConfig, useWorkspace } from "../../lib/context";
66
import React, { PropsWithChildren, useMemo, useState } from "react";
@@ -9,7 +9,7 @@ import { FaExternalLinkAlt, FaSpinner, FaTrash, FaWrench } from "react-icons/fa"
99
import { branding } from "../../lib/branding";
1010
import { useRouter } from "next/router";
1111
import { TrackingIntegrationDocumentation } from "../../components/TrackingIntegrationDocumentation/TrackingIntegrationDocumentation";
12-
import { BrowserKeysEditor } from "../../components/ApiKeyEditor/ApiKeyEditor";
12+
import { StreamKeysEditor } from "../../components/ApiKeyEditor/ApiKeyEditor";
1313
import { useQuery } from "@tanstack/react-query";
1414
import { getEeClient } from "../../lib/ee-client";
1515
import { requireDefined } from "juava";
@@ -510,19 +510,43 @@ const StreamsList: React.FC<{}> = () => {
510510
},
511511
},
512512
],
513+
onTest: async (stream: StreamConfig) => {
514+
if (stream.strict) {
515+
if (
516+
(!stream.privateKeys || stream.privateKeys.length === 0) &&
517+
(!stream.publicKeys || stream.publicKeys.length === 0)
518+
) {
519+
return { ok: false, error: "At least one writeKey required in Strict Mode." };
520+
}
521+
}
522+
return { ok: true };
523+
},
513524
fields: {
514525
type: { constant: "stream" },
515526
workspaceId: { constant: workspace.id },
527+
strict: {
528+
editor: CustomCheckbox,
529+
displayName: "Strict Mode",
530+
advanced: false,
531+
documentation: (
532+
<>
533+
In Strict Mode, Jitsu requires a valid <b>writeKey</b> to ingest events into the current stream.
534+
<br />
535+
Without Strict Mode, if a correct writeKey is not provided, Jitsu may attempt to identify the stream based
536+
on the domain or, if there is only one stream in the workspace, it will automatically select that stream.
537+
</>
538+
),
539+
},
516540
privateKeys: {
517-
editor: BrowserKeysEditor,
541+
editor: StreamKeysEditor,
518542
displayName: "Server-to-server Write Keys",
519543
advanced: false,
520544
documentation: (
521545
<>Those keys should be kept in private and used only for server-to-server calls, such as HTTP Event API</>
522546
),
523547
},
524548
publicKeys: {
525-
editor: BrowserKeysEditor,
549+
editor: StreamKeysEditor,
526550
displayName: "Browser Write Keys",
527551
advanced: false,
528552
documentation: (

webapps/console/pages/api/[workspaceId]/config/[type]/[id].ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ export const api: Api = {
6262
if (!object) {
6363
throw new ApiError(`${type} with id ${id} does not exist`);
6464
}
65-
const data = parseObject(type, body);
66-
const merged = configObjectType.merge(object.config, data);
67-
const filtered = await configObjectType.inputFilter(merged, "update");
65+
const merged = configObjectType.merge(object.config, { ...body, id, workspaceId });
66+
const data = parseObject(type, merged);
67+
const filtered = await configObjectType.inputFilter(data, "update");
6868

6969
delete filtered.id;
7070
delete filtered.workspaceId;

0 commit comments

Comments
 (0)