249 lines · 7.4 KB · TypeScript
RawDownload
import { useEffect, useMemo, useState } from "react";import type { ReactNode } from "react"; import { ScopeId } from "@executor-js/sdk"; import { useScope, useUserScope } from "../api/scope-context";import {  CardStack,  CardStackContent,  CardStackEntry,  CardStackEntryContent,  CardStackEntryDescription,  CardStackEntryTitle,} from "../components/card-stack";import { FilterTabs } from "../components/filter-tabs";import { FieldLabel } from "../components/field";import { HelpTooltip } from "../components/help-tooltip";import {  Select,  SelectContent,  SelectItem,  SelectTrigger,  SelectValue,} from "../components/select"; export interface CredentialTargetScopeOption {  readonly scopeId: ScopeId;  readonly label: string;  readonly description: string;} export const credentialTargetScopeOptions = (input: {  readonly sourceScope: ScopeId;  readonly userScope: ScopeId;  readonly sourceScopeLabel?: string;}): readonly CredentialTargetScopeOption[] => {  if (input.sourceScope === input.userScope) {    return [      {        scopeId: input.userScope,        label: "Personal",        description: "Saved only for your account.",      },    ];  }   return [    {      scopeId: input.userScope,      label: "Personal",      description: "Saved only for your account.",    },    {      scopeId: input.sourceScope,      label: input.sourceScopeLabel ?? "Organization",      description: "Shared with everyone who can use this source.",    },  ];}; export const normalizeCredentialTargetScope = (  value: ScopeId,  options: readonly CredentialTargetScopeOption[],): ScopeId => options.find((option) => option.scopeId === value)?.scopeId ?? options[0]!.scopeId; export function useCredentialTargetScope(input?: {  readonly sourceScope?: ScopeId;  readonly sourceScopeLabel?: string;  readonly initialTargetScope?: ScopeId;}): {  readonly credentialTargetScope: ScopeId;  readonly setCredentialTargetScope: (scope: ScopeId) => void;  readonly credentialScopeOptions: readonly CredentialTargetScopeOption[];} {  const routeScope = useScope();  const sourceScope = input?.sourceScope ?? routeScope;  const userScope = useUserScope();  const credentialScopeOptions = useMemo(    () =>      credentialTargetScopeOptions({        sourceScope,        userScope,        sourceScopeLabel: input?.sourceScopeLabel,      }),    [sourceScope, userScope, input?.sourceScopeLabel],  );  const initialTargetScope = input?.initialTargetScope ?? credentialScopeOptions[0]!.scopeId;  const [credentialTargetScope, setCredentialTargetScope] = useState<ScopeId>(    normalizeCredentialTargetScope(initialTargetScope, credentialScopeOptions),  );   useEffect(() => {    setCredentialTargetScope((current) =>      normalizeCredentialTargetScope(        current === routeScope && input?.sourceScope !== undefined ? initialTargetScope : current,        credentialScopeOptions,      ),    );  }, [credentialScopeOptions, initialTargetScope, input?.sourceScope, routeScope]);   return {    credentialTargetScope,    setCredentialTargetScope,    credentialScopeOptions,  };} export function CredentialTargetScopeSelector(props: {  readonly value: ScopeId;  readonly options: readonly CredentialTargetScopeOption[];  readonly onChange: (scope: ScopeId) => void;  readonly title?: string;  readonly description?: string;}) {  if (props.options.length <= 1) return null;   const active = props.options.find((option) => option.scopeId === props.value);   return (    <CardStack>      <CardStackContent className="border-t-0">        <CardStackEntry>          <CardStackEntryContent>            <CardStackEntryTitle>{props.title ?? "Credentials"}</CardStackEntryTitle>            <CardStackEntryDescription>              {props.description ??                active?.description ??                "Choose where new credentials are saved."}            </CardStackEntryDescription>          </CardStackEntryContent>          <FilterTabs<ScopeId>            tabs={props.options.map((option) => ({              value: option.scopeId,              label: option.label,            }))}            value={props.value}            onChange={props.onChange}          />        </CardStackEntry>      </CardStackContent>    </CardStack>  );} export function CredentialScopeSection(props: {  readonly value: ScopeId;  readonly options: readonly CredentialTargetScopeOption[];  readonly onChange: (scope: ScopeId) => void;  readonly children: ReactNode;  readonly title?: string;  readonly description?: string;}) {  return (    <div className="space-y-3">      <CredentialTargetScopeSelector        value={props.value}        options={props.options}        onChange={props.onChange}        title={props.title ?? "Used by"}        description={props.description ?? "Choose who can use these credentials."}      />      {props.children}    </div>  );} export function CredentialScopeDropdown(props: {  readonly value: ScopeId;  readonly options: readonly CredentialTargetScopeOption[];  readonly onChange: (scope: ScopeId) => void;  readonly label?: string;  readonly help?: ReactNode;  readonly className?: string;  readonly triggerClassName?: string;  readonly size?: "sm" | "default";}) {  const label = props.label ?? "Used by";  if (props.options.length <= 1) return null;   return (    <div className={props.className ?? "space-y-1.5"}>      <div className="flex items-center gap-1.5">        <FieldLabel className="text-[11px]">{label}</FieldLabel>        <HelpTooltip label={label}>          {props.help ?? "Choose who can use these credentials."}        </HelpTooltip>      </div>      <Select        value={String(props.value)}        onValueChange={(value) => props.onChange(ScopeId.make(value))}      >        <SelectTrigger className={props.triggerClassName ?? "w-full"} size={props.size}>          <SelectValue placeholder={label} />        </SelectTrigger>        <SelectContent>          {props.options.map((option) => (            <SelectItem key={option.scopeId} value={option.scopeId}>              {option.label}            </SelectItem>          ))}        </SelectContent>      </Select>    </div>  );} export function CredentialControlField(props: {  readonly label: string;  readonly children: ReactNode;  readonly help?: ReactNode;}) {  return (    <div className="space-y-1.5">      <div className="flex items-center gap-1.5">        <FieldLabel className="text-[11px]">{props.label}</FieldLabel>        {props.help ? <HelpTooltip label={props.label}>{props.help}</HelpTooltip> : null}      </div>      {props.children}    </div>  );} export function CredentialUsageRow(props: {  readonly value: ScopeId;  readonly options: readonly CredentialTargetScopeOption[];  readonly onChange: (scope: ScopeId) => void;  readonly children: ReactNode;  readonly label?: string;  readonly help?: ReactNode;}) {  if (props.options.length <= 1) {    return <div className="space-y-2.5">{props.children}</div>;  }   return (    <div className="grid items-stretch gap-2 md:grid-cols-2">      <div className="min-w-0 space-y-2.5">{props.children}</div>      <CredentialScopeDropdown        value={props.value}        options={props.options}        onChange={props.onChange}        label={props.label}        help={props.help}        className="flex h-full flex-col space-y-1.5"        triggerClassName="w-full min-h-9 flex-1 data-[size=default]:h-full"      />    </div>  );}