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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to

### Added

- Add missing adaptor and credential tooltips to the collab editor
[#3919](https://github.com/OpenFn/lightning/issues/3919)
- Show server validation errors in the collab editor forms
[#3783](https://github.com/OpenFn/lightning/issues/3783)
- Enforce readonly state in collaborative editor forms for viewers and old
Expand Down
89 changes: 50 additions & 39 deletions assets/js/collaborative-editor/components/ConfigureAdaptorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ import {
DialogBackdrop,
DialogPanel,
DialogTitle,
} from "@headlessui/react";
import { useEffect, useMemo, useState, useRef } from "react";
import { useHotkeysContext } from "react-hotkeys-hook";
} from '@headlessui/react';
import { useEffect, useMemo, useState, useRef } from 'react';
import { useHotkeysContext } from 'react-hotkeys-hook';

import { HOTKEY_SCOPES } from "#/collaborative-editor/constants/hotkeys";
import { HOTKEY_SCOPES } from '#/collaborative-editor/constants/hotkeys';
import {
useCredentials,
useCredentialQueries,
} from "#/collaborative-editor/hooks/useCredentials";
import type { Adaptor } from "#/collaborative-editor/types/adaptor";
import type { CredentialWithType } from "#/collaborative-editor/types/credential";
} from '#/collaborative-editor/hooks/useCredentials';
import type { Adaptor } from '#/collaborative-editor/types/adaptor';
import type { CredentialWithType } from '#/collaborative-editor/types/credential';
import {
extractAdaptorName,
extractAdaptorDisplayName,
extractPackageName,
} from "#/collaborative-editor/utils/adaptorUtils";
} from '#/collaborative-editor/utils/adaptorUtils';

import { AdaptorIcon } from "./AdaptorIcon";
import { VersionPicker } from "./VersionPicker";
import { AdaptorIcon } from './AdaptorIcon';
import { Tooltip } from './Tooltip';
import { VersionPicker } from './VersionPicker';

interface ConfigureAdaptorModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -98,10 +99,10 @@ export function ConfigureAdaptorModal({
if (adaptor) {
const sortedVersions = adaptor.versions
.map(v => v.version)
.filter(v => v !== "latest")
.filter(v => v !== 'latest')
.sort((a, b) => {
const aParts = a.split(".").map(Number);
const bParts = b.split(".").map(Number);
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aNum = aParts[i] || 0;
const bNum = bParts[i] || 0;
Expand Down Expand Up @@ -138,7 +139,7 @@ export function ConfigureAdaptorModal({
const adaptorNeedsCredentials = useMemo(() => {
const adaptorName = extractAdaptorName(currentAdaptor);
// Common adaptor doesn't require credentials
return adaptorName !== "common";
return adaptorName !== 'common';
}, [currentAdaptor]);

// Filter credentials into sections
Expand All @@ -159,10 +160,10 @@ export function ConfigureAdaptorModal({
if (c.schema === adaptorName) return true;

// Smart OAuth matching: if credential is OAuth, check oauth_client_name
if (c.schema === "oauth" && c.oauth_client_name) {
if (c.schema === 'oauth' && c.oauth_client_name) {
// Normalize both strings: lowercase, remove spaces/hyphens/underscores
const normalizeString = (str: string) =>
str.toLowerCase().replace(/[\s\-_]/g, "");
str.toLowerCase().replace(/[\s\-_]/g, '');

const normalizedClientName = normalizeString(c.oauth_client_name);
const normalizedAdaptorName = normalizeString(adaptorName);
Expand All @@ -177,22 +178,22 @@ export function ConfigureAdaptorModal({

return false;
})
.map(c => ({ ...c, type: "project" as const }));
.map(c => ({ ...c, type: 'project' as const }));

// Universal project credentials (http and raw work with all adaptors)
// Only show if not already in schemaMatched (avoid duplicates)
const universal: CredentialWithType[] = projectCredentials
.filter(c => {
const isUniversal = c.schema === "http" || c.schema === "raw";
const alreadyMatched = adaptorName === "http" || adaptorName === "raw";
const isUniversal = c.schema === 'http' || c.schema === 'raw';
const alreadyMatched = adaptorName === 'http' || adaptorName === 'raw';
return isUniversal && !alreadyMatched;
})
.map(c => ({ ...c, type: "project" as const }));
.map(c => ({ ...c, type: 'project' as const }));

// All keychain credentials (can't reliably filter by schema)
const keychain: CredentialWithType[] = keychainCredentials.map(c => ({
...c,
type: "keychain" as const,
type: 'keychain' as const,
}));

return { schemaMatched, universal, keychain };
Expand Down Expand Up @@ -253,11 +254,11 @@ export function ConfigureAdaptorModal({
// Sort versions using semantic versioning (newest first)
const sortedVersions = adaptor.versions
.map(v => v.version)
.filter(v => v !== "latest")
.filter(v => v !== 'latest')
.sort((a, b) => {
// Split version strings into parts [major, minor, patch]
const aParts = a.split(".").map(Number);
const bParts = b.split(".").map(Number);
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);

// Compare major, minor, patch in order
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
Expand All @@ -271,7 +272,7 @@ export function ConfigureAdaptorModal({
});

// Add "latest" as the first option
return ["latest", ...sortedVersions];
return ['latest', ...sortedVersions];
}, [currentAdaptor, allAdaptors]);

// Extract adaptor display name
Expand Down Expand Up @@ -352,12 +353,17 @@ export function ConfigureAdaptorModal({
font-medium text-gray-700 mb-2"
>
Adaptor
<span
className="hero-information-circle h-4 w-4
text-gray-400"
aria-label="Information"
role="img"
/>
<Tooltip
content="Choose an adaptor to perform operations (via helper functions) in a specific application. Pick 'http' for generic REST APIs or the 'common' adaptor if this job only performs data manipulation."
side="top"
>
<span
className="hero-information-circle h-4 w-4
text-gray-400"
aria-label="Information"
role="img"
/>
</Tooltip>
</label>
<div
className="flex items-center justify-between p-3
Expand Down Expand Up @@ -409,12 +415,17 @@ export function ConfigureAdaptorModal({
font-medium text-gray-700"
>
Credentials
<span
className="hero-information-circle h-4 w-4
text-gray-400"
aria-label="Information"
role="img"
/>
<Tooltip
content="If the system you're working with requires authentication, choose a credential with login details (secrets) that will allow this job to connect. If you're not connecting to an external system you don't need a credential."
side="top"
>
<span
className="hero-information-circle h-4 w-4
text-gray-400"
aria-label="Information"
role="img"
/>
</Tooltip>
</label>
{adaptorNeedsCredentials && (
<button
Expand Down Expand Up @@ -492,7 +503,7 @@ export function ConfigureAdaptorModal({
{cred.name}
</span>
</div>
{cred.type === "project" && cred.owner && (
{cred.type === 'project' && cred.owner && (
<div className="flex items-center gap-1 text-sm text-gray-500">
<span
className="hero-user-solid h-4 w-4"
Expand Down Expand Up @@ -589,7 +600,7 @@ export function ConfigureAdaptorModal({
{cred.name}
</span>
</div>
{cred.type === "project" &&
{cred.type === 'project' &&
cred.owner && (
<div className="flex items-center gap-1 text-sm text-gray-500">
<span
Expand Down
14 changes: 7 additions & 7 deletions assets/js/collaborative-editor/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as RadixTooltip from "@radix-ui/react-tooltip";
import type { ReactNode } from "react";
import * as RadixTooltip from '@radix-ui/react-tooltip';
import type { ReactNode } from 'react';

/**
* Tooltip component using Radix UI primitives
Expand All @@ -15,14 +15,14 @@ import type { ReactNode } from "react";
export function Tooltip({
children,
content,
side = "bottom",
align = "center",
side = 'bottom',
align = 'center',
delayDuration = 200,
}: {
children: ReactNode;
content: string;
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
side?: 'top' | 'right' | 'bottom' | 'left';
align?: 'start' | 'center' | 'end';
delayDuration?: number;
}) {
return (
Expand All @@ -33,7 +33,7 @@ export function Tooltip({
<RadixTooltip.Content
side={side}
align={align}
className="z-50 overflow-hidden rounded-md bg-gray-900
className="z-50 max-w-xs overflow-hidden rounded-md bg-gray-900
px-3 py-1.5 text-xs text-white shadow-md
animate-in fade-in-0 zoom-in-95
data-[state=closed]:animate-out
Expand Down
16 changes: 11 additions & 5 deletions assets/js/collaborative-editor/components/inspector/JobForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { Workflow } from '#/collaborative-editor/types/workflow';
import { AdaptorDisplay } from '../AdaptorDisplay';
import { AdaptorSelectionModal } from '../AdaptorSelectionModal';
import { ConfigureAdaptorModal } from '../ConfigureAdaptorModal';
import { Tooltip } from '../Tooltip';
import { createZodValidator } from '../form/createZodValidator';

interface JobFormProps {
Expand Down Expand Up @@ -366,11 +367,16 @@ export function JobForm({ job }: JobFormProps) {
<div className="col-span-6">
<label className="flex items-center gap-1 text-sm font-medium text-gray-700 mb-2">
Adaptor
<span
className="hero-information-circle h-4 w-4 text-gray-400"
aria-label="Information"
role="img"
/>
<Tooltip
content="Choose an adaptor to perform operations (via helper functions) in a specific application. Pick 'http' for generic REST APIs or the 'common' adaptor if this job only performs data manipulation."
side="top"
>
<span
className="hero-information-circle h-4 w-4 text-gray-400"
aria-label="Information"
role="img"
/>
</Tooltip>
</label>
<AdaptorDisplay
adaptor={currentAdaptor}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export function TriggerForm({ trigger }: TriggerFormProps) {
const sessionContext = useSessionContext();
const { provider } = useSession();
const channel = provider?.channel;
const { isReadOnly } = useWorkflowReadOnly();

// Get active trigger auth methods from workflow store
const activeTriggerAuthMethods = useWorkflowState(
Expand Down