Skip to content

Commit

Permalink
console: Streams added Strict toggle (#1123)
Browse files Browse the repository at this point in the history
* console: Streams added Strict toggle
* console: EventsBrowser: choose correct ingest type icon
* console: improve Stream Api key editor, automatically save added write keys
  • Loading branch information
absorbb authored Sep 7, 2024
1 parent f276c7c commit 7dd1f74
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 15 deletions.
138 changes: 135 additions & 3 deletions webapps/console/components/ApiKeyEditor/ApiKeyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { ApiKey } from "../../lib/schema";
import { useState } from "react";
import { Button, Table, Tooltip } from "antd";
import { branding } from "../../lib/branding";
import { confirmOp, copyTextToClipboard } from "../../lib/ui";
import { confirmOp, copyTextToClipboard, feedbackSuccess } from "../../lib/ui";
import { FaCopy, FaPlus, FaTrash } from "react-icons/fa";
import { randomId } from "juava";
import { WidgetProps } from "@rjsf/utils";
import { getConfigApi } from "../../lib/useApi";
import { useWorkspace } from "../../lib/context";
import { RefreshCw } from "lucide-react";
import { CustomWidgetProps } from "../ConfigObjectEditor/Editors";

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

export const BrowserKeysEditor: React.FC<CustomWidgetProps<ApiKey[]> & { compact?: boolean }> = (props: any) => {
return <ApiKeysEditor {...props} compact={true} />;
export const StreamKeysEditor: React.FC<WidgetProps<ApiKey[]>> = props => {
const context = props.formContext;
const workspace = useWorkspace();
const type = context?.type;
const [loading, setLoading] = useState(false);
const [keys, setKeys] = useState<ApiKey[]>(props.value || []);
const columns: any[] = [
{
title: "Key",
key: "id",
width: "90%",
render: (key: ApiKey) => {
return key.plaintext && key.id !== "Generating key" ? (
<div className="flex items-center">
<Tooltip
className="cursor-pointer"
title={
<>
{" "}
<strong>Key generated!</strong> Copy this key and store it in a safe place. You will not be able to
see it again.
</>
}
>
<code>
{key.id}:{key.plaintext}
</code>
</Tooltip>
<CopyToClipboard text={`${key.id}:${key.plaintext}`} />
</div>
) : (
<>
<Tooltip
className="cursor-pointer"
title={
<>
{branding.productName} doesn't store full version of a key. If you haven't recorded the key, generate
a new one
</>
}
>
<code>
{key.id}:{key?.hint?.replace("*", "*".repeat(32 - 6))}
</code>
</Tooltip>
</>
);
},
},
];
columns.push({
title: "",
className: "text-right",
key: "actions",
render: (key: ApiKey) => {
if (key.id === "Generating key") {
return <></>;
}
return (
<div>
<Button
type="text"
onClick={async () => {
if (
key.plaintext ||
(await confirmOp("Are you sure you want to delete this API key? You won't be able to recover it"))
) {
const newVal = keys.filter(k => k.id !== key.id);
setKeys(newVal);
props.onChange(newVal);
}
}}
>
<FaTrash />
</Button>
</div>
);
},
});
return (
<div className={"pt-3"}>
{keys.length === 0 && !loading && <div className="flex text-textDisabled justify-center">Keys list is empty</div>}
{(keys.length > 0 || loading) && (
<Table
size={"small"}
showHeader={false}
columns={columns}
dataSource={loading ? [...keys, { id: "Generating key", hint: "..." }] : keys}
pagination={false}
rowKey={k => k.id}
/>
)}
<div className="flex justify-between p-2">
{keys.find(key => !!key.plaintext) ? (
<div className="text-text text-sm">
Congrats! You're generated a new key(s). Copy it an keep in the safe place.
<br />
You will not be able to see it again once you leave the page
</div>
) : (
<div></div>
)}
<Button
type="text"
onClick={async () => {
try {
const newKey = randomId(32);
const newVal = [...keys, { id: randomId(32), plaintext: newKey, hint: hint(newKey) }];
if (type === "stream" && !context.isNew) {
setLoading(true);
await getConfigApi(workspace.id, "stream").update(context.object.id, {
[props.name]: newVal,
});
setKeys(newVal);
props.onChange(newVal);
feedbackSuccess("Write Key Saved");
} else {
setKeys(newVal);
props.onChange(newVal);
}
} finally {
setLoading(false);
}
}}
>
{loading ? <RefreshCw className={"w-4 h-4 animate-spin"} /> : <FaPlus />}
</Button>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ const FormList: React.FC<ObjectFieldTemplateProps> = props => {
);
};

const CustomCheckbox = function (props) {
export const CustomCheckbox = function (props) {
return <Switch checked={props.value} onClick={() => props.onChange(!props.value)} />;
};

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

const uiSchema = getUiSchema(schema, fields);
Expand All @@ -257,6 +257,7 @@ const EditorComponent: React.FC<EditorComponentProps> = props => {
const onFormChange = state => {
setFormState(state);
setTestResult(undefined);
setTouched(true);
log.atDebug().log(`Updating editor form state`, state);
};
const withLoading = (fn: () => Promise<void>) => async () => {
Expand Down Expand Up @@ -291,11 +292,11 @@ const EditorComponent: React.FC<EditorComponentProps> = props => {
liveValidate={true}
validator={validator}
onSubmit={async ({ formData }) => {
if (onTest && testConnectionEnabled && testConnectionEnabled(formData || object)) {
if (onTest && (typeof testConnectionEnabled === "undefined" || testConnectionEnabled(formData || object))) {
const testRes = testResult || (await onTest(formState?.formData || object));
if (!testRes.ok) {
modal.confirm({
title: "Connection test failed",
title: "Check failed",
content: testRes.error,
okText: "Save anyway",
okType: "danger",
Expand Down
2 changes: 1 addition & 1 deletion webapps/console/components/DataView/EventsBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ const IncomingEventsTable = ({ loadEvents, loading, streamType, entityType, acto
//dataIndex: "type",
render: (d: IncomingEvent) => {
const eventName = d.type === "track" ? d.event?.event || d.type : d.type;
const isDeviceEvent = d.pagePath;
const isDeviceEvent = d.ingestType === "browser";
return (
<Tooltip title={eventName}>
<Tag
Expand Down
1 change: 1 addition & 0 deletions webapps/console/lib/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export const StreamConfig = ConfigEntityBase.merge(
authorizedJavaScriptDomains: z.string().optional(),
publicKeys: z.array(ApiKey).optional(),
privateKeys: z.array(ApiKey).optional(),
strict: z.boolean().optional(),
})
);
export type StreamConfig = z.infer<typeof StreamConfig>;
Expand Down
32 changes: 28 additions & 4 deletions webapps/console/pages/[workspaceId]/streams.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { WorkspacePageLayout } from "../../components/PageLayout/WorkspacePageLayout";
import { Button, Input, notification, Tag, Tooltip } from "antd";
import { ConfigEditor, ConfigEditorProps } from "../../components/ConfigObjectEditor/ConfigEditor";
import { ConfigEditor, ConfigEditorProps, CustomCheckbox } from "../../components/ConfigObjectEditor/ConfigEditor";
import { StreamConfig } from "../../lib/schema";
import { useAppConfig, useWorkspace } from "../../lib/context";
import React, { PropsWithChildren, useMemo, useState } from "react";
Expand All @@ -9,7 +9,7 @@ import { FaExternalLinkAlt, FaSpinner, FaTrash, FaWrench } from "react-icons/fa"
import { branding } from "../../lib/branding";
import { useRouter } from "next/router";
import { TrackingIntegrationDocumentation } from "../../components/TrackingIntegrationDocumentation/TrackingIntegrationDocumentation";
import { BrowserKeysEditor } from "../../components/ApiKeyEditor/ApiKeyEditor";
import { StreamKeysEditor } from "../../components/ApiKeyEditor/ApiKeyEditor";
import { useQuery } from "@tanstack/react-query";
import { getEeClient } from "../../lib/ee-client";
import { requireDefined } from "juava";
Expand Down Expand Up @@ -510,19 +510,43 @@ const StreamsList: React.FC<{}> = () => {
},
},
],
onTest: async (stream: StreamConfig) => {
if (stream.strict) {
if (
(!stream.privateKeys || stream.privateKeys.length === 0) &&
(!stream.publicKeys || stream.publicKeys.length === 0)
) {
return { ok: false, error: "At least one writeKey required in Strict Mode." };
}
}
return { ok: true };
},
fields: {
type: { constant: "stream" },
workspaceId: { constant: workspace.id },
strict: {
editor: CustomCheckbox,
displayName: "Strict Mode",
advanced: false,
documentation: (
<>
In Strict Mode, Jitsu requires a valid <b>writeKey</b> to ingest events into the current stream.
<br />
Without Strict Mode, if a correct writeKey is not provided, Jitsu may attempt to identify the stream based
on the domain or, if there is only one stream in the workspace, it will automatically select that stream.
</>
),
},
privateKeys: {
editor: BrowserKeysEditor,
editor: StreamKeysEditor,
displayName: "Server-to-server Write Keys",
advanced: false,
documentation: (
<>Those keys should be kept in private and used only for server-to-server calls, such as HTTP Event API</>
),
},
publicKeys: {
editor: BrowserKeysEditor,
editor: StreamKeysEditor,
displayName: "Browser Write Keys",
advanced: false,
documentation: (
Expand Down
6 changes: 3 additions & 3 deletions webapps/console/pages/api/[workspaceId]/config/[type]/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export const api: Api = {
if (!object) {
throw new ApiError(`${type} with id ${id} does not exist`);
}
const data = parseObject(type, body);
const merged = configObjectType.merge(object.config, data);
const filtered = await configObjectType.inputFilter(merged, "update");
const merged = configObjectType.merge(object.config, { ...body, id, workspaceId });
const data = parseObject(type, merged);
const filtered = await configObjectType.inputFilter(data, "update");

delete filtered.id;
delete filtered.workspaceId;
Expand Down

0 comments on commit 7dd1f74

Please sign in to comment.