Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 18 additions & 26 deletions apps/portal/src/components/canvas/CanvasWorkbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export type CanvasWorkbenchProps = {
/** Opt-in only: the route owning the playground controls enables its trigger. */
enablePlayground?: boolean;
playgroundContent?: ReactNode;
/** Extra header controls owned by the caller — route settings, for one. */
headerActions?: ReactNode;
/** Replaces the plain title on the left — the route switcher, for one. */
headerLeft?: ReactNode;
/** re-read from the server, for diagnosing a rejected save */
reload: () => Promise<CanvasItems>;
save: (payload: CanvasSavePayload) => Promise<unknown>;
Expand Down Expand Up @@ -50,8 +54,9 @@ export function CanvasWorkbench({
enableBlockPicker = false,
enablePlayground = false,
playgroundContent,
headerActions,
headerLeft,
}: CanvasWorkbenchProps) {
const [readOnly, setReadOnly] = useState(false);
const [pendingCount, setPendingCount] = useState(0);
const [isSaving, setIsSaving] = useState(false);
const [cycleFeedbackToken, setCycleFeedbackToken] = useState(0);
Expand Down Expand Up @@ -94,30 +99,17 @@ export function CanvasWorkbench({

return (
<div className="flex h-screen w-full flex-col">
<header className="flex items-center gap-4 border-b border-border px-4 py-2 text-sm">
<span className="font-medium">{title}</span>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={readOnly}
onChange={(e) => setReadOnly(e.target.checked)}
/>
Read only
</label>
<span className="text-muted">
{graph.blocks.length} blocks · {graph.edges.length} edges ·{" "}
{pendingCount} unsaved
</span>
{!readOnly && (
<Button
variant="primary"
isDisabled={pendingCount === 0 || isSaving}
isPending={isSaving}
onPress={() => void onSave()}
>
Save
</Button>
)}
<header className="flex items-center gap-3 border-b border-border px-4 py-2 text-sm">
{headerLeft ?? <span className="font-medium">{title}</span>}
<div className="ml-auto flex items-center gap-2">{headerActions}</div>
<Button
variant="primary"
isDisabled={pendingCount === 0 || isSaving}
isPending={isSaving}
onPress={() => void onSave()}
>
Save
</Button>
</header>

<div className="min-h-0 flex-1">
Expand All @@ -132,7 +124,7 @@ export function CanvasWorkbench({
) : (
<BlockCanvas
graph={graph}
mode={readOnly ? "readonly" : "edit"}
mode="edit"
nodeTypes={nodeTypes}
enableBlockPicker={enableBlockPicker}
enablePlayground={enablePlayground}
Expand Down
11 changes: 10 additions & 1 deletion apps/portal/src/components/integrations/IntegrationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Spinner, toast } from "@fluxify/components";
import { TbTrash } from "react-icons/tb";
import { TbBolt, TbTrash } from "react-icons/tb";
import {
getIntegrationsGroups,
getIntegrationsVariants,
Expand Down Expand Up @@ -209,6 +209,15 @@ export function IntegrationForm({
<TbTrash size={15} />
</button>
)}
<Button
variant="outline"
isPending={test.isPending}
onPress={onTest}
className="whitespace-nowrap"
>
<TbBolt size={14} className="text-[#D0F237]" />
<span>Test connection</span>
</Button>
<Button variant="primary" isPending={create.isPending || update.isPending} onPress={onSave}>
{id ? "Save changes" : "Connect"}
</Button>
Expand Down
227 changes: 183 additions & 44 deletions apps/portal/src/components/integrations/IntegrationOnboardingForm.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export function IntegrationOnboardingModal({ projectId, isOpen, onOpenChange }:
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<Modal.Backdrop>
<Modal.Container placement="center" scroll="inside" size="lg">
<Modal.Dialog className="w-full !max-w-3xl border border-[#1C202B] bg-[#0E1015]">
<Modal.Header className="px-5 pb-3 pt-5 sm:px-7">
<Modal.Dialog className="w-full !max-w-2xl border border-[#1C202B] bg-[#0E1015]">
<Modal.Header className="px-6 pb-2 pt-5">
<Modal.Heading className="text-white">Connect an integration</Modal.Heading>
</Modal.Header>
<Modal.Body className="px-5 pb-5 sm:px-7 sm:pb-7">
<Modal.Body className="px-6 pb-6 pt-1">
{isOpen && <IntegrationOnboardingForm projectId={projectId} onSaved={() => onOpenChange(false)} />}
</Modal.Body>
</Modal.Dialog>
Expand Down
22 changes: 11 additions & 11 deletions apps/portal/src/components/integrations/connectors/AiForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Input, Label } from "@fluxify/components";
import { Input } from "@fluxify/components";
import { AppConfigSelector } from "../AppConfigSelector";
import type { ConnectorFormProps } from "./types";

Expand All @@ -12,11 +12,12 @@ export function AiForm({
showBaseUrl = false,
}: ConnectorFormProps & { showBaseUrl?: boolean }) {
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3.5">
<div className="flex flex-col gap-1">
<Label>Name *</Label>
<span className="text-xs text-muted">Unique Name for the integration</span>
<Input value={name} onChange={(e) => onName(e.currentTarget.value)} placeholder="name" />
<label className="text-xs font-medium text-zinc-300">
Integration Name <span className="text-[#D0F237]">*</span>
</label>
<Input value={name} onChange={(e) => onName(e.currentTarget.value)} placeholder="e.g. Production OpenAI" />
</div>

{showBaseUrl && (
Expand All @@ -25,7 +26,6 @@ export function AiForm({
value={(config.baseUrl as string) ?? ""}
onChange={(v) => setField("baseUrl", v)}
label="Base URL"
description="Base URL for the AI"
placeholder="https://api.openai.com/v1"
/>
)}
Expand All @@ -34,16 +34,16 @@ export function AiForm({
value={(config.apiKey as string) ?? ""}
onChange={(v) => setField("apiKey", v)}
label="API Key"
description="API Key for the AI"
placeholder="api-key"
placeholder="sk-..."
/>
<div className="flex flex-col gap-1">
<Label>Model *</Label>
<span className="text-xs text-muted">Model for the AI</span>
<label className="text-xs font-medium text-zinc-300">
Model <span className="text-[#D0F237]">*</span>
</label>
<Input
value={(config.model as string) ?? ""}
onChange={(e) => setField("model", e.currentTarget.value)}
placeholder="model-name"
placeholder="e.g. gpt-4o, claude-3-5-sonnet, gemini-1.5-pro"
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react";
import { Button, Checkbox, Input, Label, cn } from "@fluxify/components";
import { Checkbox, Input, cn } from "@fluxify/components";
import { AppConfigSelector } from "../AppConfigSelector";
import type { ConnectorFormProps } from "./types";

Expand Down Expand Up @@ -34,31 +34,36 @@ export function CredentialsUrlForm({
const isUrl = tab === "url";

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3.5">
<div className="flex flex-col gap-1">
<Label>Name *</Label>
<span className="text-xs text-muted">Unique Name for the integration</span>
<label className="text-xs font-medium text-zinc-300">
Integration Name <span className="text-[#D0F237]">*</span>
</label>
<Input
value={name}
onChange={(e) => onName(e.currentTarget.value)}
placeholder={placeholders.name}
/>
</div>

<div className="flex gap-1.5 rounded-md border border-border p-1">
<div className="flex rounded-lg border border-[#232836] bg-[#141720] p-1">
{(["credentials", "url"] as const).map((t) => (
<Button
<button
key={t}
type="button"
fullWidth
variant={tab === t ? "primary" : "ghost"}
onPress={() => {
onClick={() => {
setField("source", t === "url" ? "url" : "credentials");
setTab(t);
}}
className={cn(
"flex-1 rounded-md py-1 text-xs font-medium transition-all duration-150",
tab === t
? "bg-[#232938] text-white shadow-sm"
: "text-zinc-400 hover:text-zinc-200",
)}
>
{t === "url" ? "Via URL" : "Credentials"}
</Button>
</button>
))}
</div>

Expand Down Expand Up @@ -102,7 +107,7 @@ export function CredentialsUrlForm({
/>
)}
{hasSSL && (
<div className={cn("flex items-end", hasDatabase ? "" : "col-span-2")}>
<div className={cn("flex items-end pb-1.5", hasDatabase ? "" : "col-span-2")}>
<Checkbox
isSelected={Boolean(config.useSSL)}
onChange={(v) => setField("useSSL", v)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Input, Label } from "@fluxify/components";
import { Input, cn } from "@fluxify/components";
import { AppConfigSelector } from "../AppConfigSelector";
import type { ConnectorFormProps } from "./types";

Expand All @@ -21,39 +21,48 @@ export function ObservabilityForm({
const creds = (config.credentials ?? {}) as { username?: string; password?: string };

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3.5">
<div className="flex flex-col gap-1">
<Label>Name *</Label>
<span className="text-xs text-muted">Unique Name for the integration</span>
<label className="text-xs font-medium text-zinc-300">
Integration Name <span className="text-[#D0F237]">*</span>
</label>
<Input value={name} onChange={(e) => onName(e.currentTarget.value)} placeholder={namePlaceholder} />
</div>

<AppConfigSelector
projectId={projectId}
value={(config.baseUrl as string) ?? ""}
onChange={(v) => setField("baseUrl", v)}
label="Base Url"
label="Base URL"
description={baseUrlDescription}
placeholder={baseUrlPlaceholder}
/>

<div className="flex gap-1.5 rounded-md border border-border p-1">
<Button
<div className="flex rounded-lg border border-[#232836] bg-[#141720] p-1">
<button
type="button"
fullWidth
variant={isCredentials ? "ghost" : "primary"}
onPress={() => setField("credentials", "")}
onClick={() => setField("credentials", "")}
className={cn(
"flex-1 rounded-md py-1 text-xs font-medium transition-all duration-150",
!isCredentials
? "bg-[#232938] text-white shadow-sm"
: "text-zinc-400 hover:text-zinc-200",
)}
>
Base64 Encoded
</Button>
<Button
</button>
<button
type="button"
fullWidth
variant={isCredentials ? "primary" : "ghost"}
onPress={() => setField("credentials", { username: "", password: "" })}
onClick={() => setField("credentials", { username: "", password: "" })}
className={cn(
"flex-1 rounded-md py-1 text-xs font-medium transition-all duration-150",
isCredentials
? "bg-[#232938] text-white shadow-sm"
: "text-zinc-400 hover:text-zinc-200",
)}
>
Credentials
</Button>
</button>
</div>

{!isCredentials ? (
Expand All @@ -72,15 +81,13 @@ export function ObservabilityForm({
value={creds.username ?? ""}
onChange={(v) => setField("credentials.username", v)}
label="Email"
description="Email / username"
placeholder="email@company.co"
/>
<AppConfigSelector
projectId={projectId}
value={creds.password ?? ""}
onChange={(v) => setField("credentials.password", v)}
label="Password"
description="Password"
placeholder="password"
/>
</div>
Expand Down
Loading
Loading