mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 22:19:40 +08:00
fix(cloud): Mark cloud-hostile components and consolidate cloud metadata utils
Mark git, google, and homeassistant components as cloud_compatible=False since they require local filesystem, credentials files, or local network access. Consolidate duplicated cloud metadata helpers into cloudMetadataUtils and fix the nodeType resolution to use the explicit prop instead of falling back to nodeClass.type. Fix model input empty state to distinguish disabled models from cloud-filtered models.
This commit is contained in:
@ -201,10 +201,10 @@ The **Write File** component's "Local" storage option is hidden from the Sortabl
|
||||
|
||||
| Directory | Component | Notes |
|
||||
|-----------|-----------|-------|
|
||||
| `google` | Gmail Loader, Google Drive Search, Google Drive, Google OAuth Token | Legacy; require local credentials files |
|
||||
| `google` | Gmail Loader, Google Drive Loader, Google Drive Search, Google OAuth Token | Legacy; require local credentials files |
|
||||
| `deactivated` | Various | Deprecated components; should not be used |
|
||||
| `homeassistant` | Home Assistant Control, List States | Requires local network Home Assistant instance (`192.168.x.x`) |
|
||||
| `git` | Git Loader, Git Extractor | Clones repos to local temp dirs; requires `git` CLI |
|
||||
| `homeassistant` | Home Assistant Control, List Home Assistant States | Requires local network Home Assistant instance (`192.168.x.x`) |
|
||||
| `git` | Git, GitExtractor | Clones repos to local temp dirs; requires `git` CLI |
|
||||
|
||||
> **Note on legacy Google components:** These are marked `legacy = True` with replacements suggested. They require local credentials JSON files which won't work on ephemeral pods. They're already hidden when "Show Legacy" is off.
|
||||
|
||||
|
||||
@ -75,9 +75,12 @@ jest.mock("@/controllers/API/queries/models/use-get-model-providers", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
let mockEnabledModelsData: { enabled_models: Record<string, Record<string, boolean>> } =
|
||||
{ enabled_models: {} };
|
||||
|
||||
jest.mock("@/controllers/API/queries/models/use-get-enabled-models", () => ({
|
||||
useGetEnabledModels: jest.fn(() => ({
|
||||
data: { enabled_models: {} },
|
||||
data: mockEnabledModelsData,
|
||||
isLoading: false,
|
||||
})),
|
||||
}));
|
||||
@ -185,6 +188,8 @@ const renderWithQueryClient = (component: React.ReactElement) => {
|
||||
describe("ModelInputComponent", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCloudOnly = false;
|
||||
mockEnabledModelsData = { enabled_models: {} };
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
@ -560,6 +565,39 @@ describe("ModelInputComponent", () => {
|
||||
mockCloudOnly = false;
|
||||
});
|
||||
|
||||
it("should keep the generic empty state when models are disabled instead of cloud-filtered", () => {
|
||||
mockCloudOnly = true;
|
||||
mockEnabledModelsData = {
|
||||
enabled_models: {
|
||||
OpenAI: {
|
||||
"gpt-4": false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithQueryClient(
|
||||
<ModelInputComponent
|
||||
{...defaultProps}
|
||||
options={[
|
||||
{
|
||||
id: "gpt-4",
|
||||
name: "gpt-4",
|
||||
icon: "Bot",
|
||||
provider: "OpenAI",
|
||||
metadata: {},
|
||||
},
|
||||
]}
|
||||
value={[]}
|
||||
showEmptyState={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("No models enabled")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("No cloud-compatible models"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show all providers when cloud mode is inactive", () => {
|
||||
mockCloudOnly = false;
|
||||
|
||||
|
||||
@ -76,29 +76,32 @@ export default function ModelInputComponent({
|
||||
|
||||
const cloudOnly = useCloudModeStore((state) => state.cloudOnly);
|
||||
|
||||
const enabledOptions = useMemo(
|
||||
() =>
|
||||
options.filter((option) => {
|
||||
if (option.metadata?.is_disabled_provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const provider = option.provider || "Unknown";
|
||||
const providerModels = enabledModelsData?.enabled_models?.[provider];
|
||||
|
||||
return providerModels?.[option.name] !== false;
|
||||
}),
|
||||
[enabledModelsData?.enabled_models, options],
|
||||
);
|
||||
|
||||
// Groups models by their provider name for sectioned display in dropdown.
|
||||
// Filters out models from disabled providers, disabled models,
|
||||
// and cloud-incompatible providers when cloud mode is active.
|
||||
// Filters out cloud-incompatible providers when cloud mode is active.
|
||||
const groupedOptions = useMemo(() => {
|
||||
const grouped: Record<string, ModelOption[]> = {};
|
||||
for (const option of options) {
|
||||
if (option.metadata?.is_disabled_provider) continue;
|
||||
for (const option of enabledOptions) {
|
||||
const provider = option.provider || "Unknown";
|
||||
|
||||
// Filter out cloud-incompatible providers when cloud mode is active
|
||||
if (cloudOnly && CLOUD_INCOMPATIBLE_PROVIDERS.has(provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter out disabled models using client-side enabled models data
|
||||
// This provides a reliable fallback when backend filtering fails
|
||||
if (enabledModelsData?.enabled_models) {
|
||||
const providerModels = enabledModelsData.enabled_models[provider];
|
||||
if (providerModels && providerModels[option.name] === false) {
|
||||
continue; // Skip disabled models
|
||||
}
|
||||
}
|
||||
|
||||
if (!grouped[provider]) {
|
||||
grouped[provider] = [];
|
||||
}
|
||||
@ -106,7 +109,7 @@ export default function ModelInputComponent({
|
||||
grouped[provider].push(option);
|
||||
}
|
||||
return grouped;
|
||||
}, [options, enabledModelsData, cloudOnly]);
|
||||
}, [enabledOptions, cloudOnly]);
|
||||
|
||||
// Flattened array of all enabled options for efficient lookups by name
|
||||
const flatOptions = useMemo(
|
||||
@ -162,13 +165,30 @@ export default function ModelInputComponent({
|
||||
[cloudOnly, selectedModel?.provider],
|
||||
);
|
||||
|
||||
const cloudFilteredOptionCount = useMemo(
|
||||
() =>
|
||||
cloudOnly
|
||||
? enabledOptions.filter((option) =>
|
||||
CLOUD_INCOMPATIBLE_PROVIDERS.has(option.provider || "Unknown"),
|
||||
).length
|
||||
: 0,
|
||||
[cloudOnly, enabledOptions],
|
||||
);
|
||||
|
||||
const showNoCompatibleCloudModels = useMemo(
|
||||
() =>
|
||||
cloudOnly &&
|
||||
options.length > 0 &&
|
||||
enabledOptions.length > 0 &&
|
||||
flatOptions.length === 0 &&
|
||||
cloudFilteredOptionCount > 0 &&
|
||||
!selectedModel,
|
||||
[cloudOnly, options.length, flatOptions.length, selectedModel],
|
||||
[
|
||||
cloudFilteredOptionCount,
|
||||
cloudOnly,
|
||||
enabledOptions.length,
|
||||
flatOptions.length,
|
||||
selectedModel,
|
||||
],
|
||||
);
|
||||
|
||||
const effectiveShowEmptyState = showEmptyState || showNoCompatibleCloudModels;
|
||||
|
||||
@ -4,6 +4,7 @@ import { ReactSortable } from "react-sortablejs";
|
||||
import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent";
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { isCloudIncompatibleOption as isCloudIncompatibleListOption } from "@/utils/cloudMetadataUtils";
|
||||
import { cn } from "@/utils/utils";
|
||||
import type { InputProps } from "../../types";
|
||||
import HelperTextComponent from "../helperTextComponent";
|
||||
@ -113,14 +114,9 @@ const SortableListComponent = ({
|
||||
// Convert value to an array if it exists, otherwise use empty array
|
||||
const listData = useMemo(() => (Array.isArray(value) ? value : []), [value]);
|
||||
|
||||
const isCloudIncompatibleOption = useCallback(
|
||||
const isCloudIncompatibleSelection = useCallback(
|
||||
(item: unknown) => {
|
||||
const itemName =
|
||||
typeof item === "object" && item !== null && "name" in item
|
||||
? ((item as { name?: unknown }).name ?? item)
|
||||
: item;
|
||||
|
||||
return cloudIncompatibleOptions.some((option) => option === itemName);
|
||||
return isCloudIncompatibleListOption(item, cloudIncompatibleOptions);
|
||||
},
|
||||
[cloudIncompatibleOptions],
|
||||
);
|
||||
@ -224,7 +220,7 @@ const SortableListComponent = ({
|
||||
index={index}
|
||||
onRemove={createRemoveHandler(index)}
|
||||
limit={limit}
|
||||
showCloudIncompatibleWarning={isCloudIncompatibleOption(data)}
|
||||
showCloudIncompatibleWarning={isCloudIncompatibleSelection(data)}
|
||||
/>
|
||||
))}
|
||||
</ReactSortable>
|
||||
|
||||
@ -12,7 +12,13 @@ import { ENABLE_INSPECTION_PANEL } from "@/customization/feature-flags";
|
||||
import { useCloudModeStore } from "@/stores/cloudModeStore";
|
||||
import { useTypesStore } from "@/stores/typesStore";
|
||||
import type { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { withCurrentCloudMetadata } from "@/utils/cloudMetadataUtils";
|
||||
import {
|
||||
filterCloudCompatibleOptions,
|
||||
getCloudFieldOverride,
|
||||
getCloudIncompatibleOptions,
|
||||
getCloudUiMetadata,
|
||||
withCurrentCloudMetadata,
|
||||
} from "@/utils/cloudMetadataUtils";
|
||||
import AccordionPromptComponent from "./components/accordionPromptComponent";
|
||||
import DictComponent from "./components/dictComponent";
|
||||
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
||||
@ -69,13 +75,12 @@ export function ParameterRenderComponent({
|
||||
const cloudOnly = useCloudModeStore((state) => state.cloudOnly);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
|
||||
const resolvedNodeType =
|
||||
nodeType ??
|
||||
(typeof nodeClass?.type === "string" ? nodeClass.type : undefined);
|
||||
const effectiveNodeClass =
|
||||
cloudOnly && resolvedNodeType
|
||||
? (withCurrentCloudMetadata(nodeClass, templates[resolvedNodeType]) ??
|
||||
nodeClass)
|
||||
cloudOnly && nodeType
|
||||
? (withCurrentCloudMetadata(
|
||||
nodeClass,
|
||||
templates[nodeType] as APIClassType | undefined,
|
||||
) ?? nodeClass)
|
||||
: nodeClass;
|
||||
|
||||
const id = (
|
||||
@ -85,15 +90,7 @@ export function ParameterRenderComponent({
|
||||
templateData.name
|
||||
).toLowerCase();
|
||||
|
||||
const nodeMetadata = effectiveNodeClass?.metadata as
|
||||
| {
|
||||
cloud_default_overrides?: Record<
|
||||
string,
|
||||
{ value?: unknown; placeholder?: string }
|
||||
>;
|
||||
cloud_incompatible_options?: Record<string, unknown[]>;
|
||||
}
|
||||
| undefined;
|
||||
const nodeMetadata = getCloudUiMetadata(effectiveNodeClass?.metadata);
|
||||
|
||||
const shouldUseCloudPlaceholder =
|
||||
cloudOnly &&
|
||||
@ -102,7 +99,7 @@ export function ParameterRenderComponent({
|
||||
templateValue === null);
|
||||
|
||||
const cloudOverride = shouldUseCloudPlaceholder
|
||||
? nodeMetadata?.cloud_default_overrides?.[name]
|
||||
? getCloudFieldOverride(nodeMetadata, name)
|
||||
: undefined;
|
||||
|
||||
const renderComponent = (): React.ReactElement<InputProps> => {
|
||||
@ -311,21 +308,16 @@ export function ParameterRenderComponent({
|
||||
}
|
||||
case "sortableList": {
|
||||
// Filter out cloud-incompatible options when cloud mode is active
|
||||
let sortableOptions = templateData?.options;
|
||||
let cloudIncompatibleOptions: unknown[] = [];
|
||||
if (cloudOnly && nodeMetadata?.cloud_incompatible_options) {
|
||||
const incompatible = nodeMetadata.cloud_incompatible_options[name];
|
||||
if (incompatible && Array.isArray(incompatible)) {
|
||||
cloudIncompatibleOptions = incompatible;
|
||||
sortableOptions = sortableOptions?.filter((opt: unknown) => {
|
||||
const optionName =
|
||||
typeof opt === "object" && opt !== null && "name" in opt
|
||||
? ((opt as { name?: unknown }).name ?? opt)
|
||||
: opt;
|
||||
return !incompatible.includes(optionName);
|
||||
});
|
||||
}
|
||||
}
|
||||
const cloudIncompatibleOptions = cloudOnly
|
||||
? getCloudIncompatibleOptions(nodeMetadata, name)
|
||||
: [];
|
||||
const sortableOptions =
|
||||
cloudOnly && cloudIncompatibleOptions.length > 0
|
||||
? filterCloudCompatibleOptions(
|
||||
templateData?.options,
|
||||
cloudIncompatibleOptions,
|
||||
)
|
||||
: templateData?.options;
|
||||
return (
|
||||
<SortableListComponent
|
||||
{...baseInputProps}
|
||||
|
||||
@ -7,77 +7,14 @@ import { useCloudModeStore } from "@/stores/cloudModeStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import type { APIClassType } from "@/types/api";
|
||||
import type { AllNodeType } from "@/types/flow";
|
||||
import {
|
||||
applyCloudDefaultOverrides,
|
||||
getCloudUiMetadata,
|
||||
sanitizeCloudIncompatibleDefaults,
|
||||
} from "@/utils/cloudMetadataUtils";
|
||||
import { getNodeId } from "@/utils/reactflowUtils";
|
||||
import { getNodeRenderType } from "@/utils/utils";
|
||||
|
||||
type CloudFieldOverride = {
|
||||
value?: unknown;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
type CloudComponentMetadata = {
|
||||
cloud_default_overrides?: Record<string, CloudFieldOverride>;
|
||||
cloud_incompatible_options?: Record<string, unknown[]>;
|
||||
};
|
||||
|
||||
const getOptionName = (option: unknown) => {
|
||||
if (typeof option === "object" && option !== null && "name" in option) {
|
||||
return (option as { name?: unknown }).name;
|
||||
}
|
||||
|
||||
return option;
|
||||
};
|
||||
|
||||
const sanitizeCloudIncompatibleDefaults = (
|
||||
component: APIClassType,
|
||||
cloudIncompatibleOptions?: Record<string, unknown[]>,
|
||||
) => {
|
||||
if (!cloudIncompatibleOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(cloudIncompatibleOptions).forEach(
|
||||
([fieldName, incompatibleOptions]) => {
|
||||
if (!Array.isArray(incompatibleOptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const templateField = component.template?.[fieldName];
|
||||
if (!templateField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOptions = Array.isArray(templateField.value)
|
||||
? templateField.value
|
||||
: templateField.value
|
||||
? [templateField.value]
|
||||
: [];
|
||||
|
||||
const filteredSelections = selectedOptions.filter(
|
||||
(selection) => !incompatibleOptions.includes(getOptionName(selection)),
|
||||
);
|
||||
|
||||
if (filteredSelections.length > 0) {
|
||||
templateField.value = filteredSelections;
|
||||
return;
|
||||
}
|
||||
|
||||
if (templateField.limit !== 1 || !Array.isArray(templateField.options)) {
|
||||
templateField.value = filteredSelections;
|
||||
return;
|
||||
}
|
||||
|
||||
const firstCompatibleOption = templateField.options.find(
|
||||
(option) => !incompatibleOptions.includes(getOptionName(option)),
|
||||
);
|
||||
|
||||
templateField.value = firstCompatibleOption
|
||||
? [cloneDeep(firstCompatibleOption)]
|
||||
: [];
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export function useAddComponent() {
|
||||
const store = useStoreApi();
|
||||
const paste = useFlowStore((state) => state.paste);
|
||||
@ -129,9 +66,7 @@ export function useAddComponent() {
|
||||
(output) => outputType && output.types.includes(outputType),
|
||||
);
|
||||
|
||||
const componentMetadata = component.metadata as
|
||||
| CloudComponentMetadata
|
||||
| undefined;
|
||||
const componentMetadata = getCloudUiMetadata(component.metadata);
|
||||
|
||||
const cloudDefaultOverrides = componentMetadata?.cloud_default_overrides;
|
||||
const cloudIncompatibleOptions =
|
||||
@ -142,26 +77,10 @@ export function useAddComponent() {
|
||||
? (() => {
|
||||
const clonedComponent = cloneDeep(component);
|
||||
|
||||
if (cloudDefaultOverrides) {
|
||||
Object.entries(cloudDefaultOverrides).forEach(
|
||||
([fieldName, override]) => {
|
||||
if (!clonedComponent.template?.[fieldName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.hasOwn(override, "value")) {
|
||||
clonedComponent.template[fieldName].value =
|
||||
override.value;
|
||||
}
|
||||
|
||||
if (override.placeholder !== undefined) {
|
||||
clonedComponent.template[fieldName].placeholder =
|
||||
override.placeholder;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
applyCloudDefaultOverrides(
|
||||
clonedComponent,
|
||||
cloudDefaultOverrides,
|
||||
);
|
||||
sanitizeCloudIncompatibleDefaults(
|
||||
clonedComponent,
|
||||
cloudIncompatibleOptions,
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { cloneDeep } from "lodash";
|
||||
import type { APIClassType } from "@/types/api";
|
||||
|
||||
type CloudFieldOverride = {
|
||||
export type CloudFieldOverride = {
|
||||
value?: unknown;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
type CloudUiMetadata = Record<string, unknown> & {
|
||||
export type CloudUiMetadata = Record<string, unknown> & {
|
||||
cloud_default_overrides?: Record<string, CloudFieldOverride>;
|
||||
cloud_incompatible_options?: Record<string, unknown[]>;
|
||||
};
|
||||
@ -14,6 +15,132 @@ function isCloudUiMetadata(value: unknown): value is CloudUiMetadata {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function getCloudUiMetadata(value: unknown): CloudUiMetadata | undefined {
|
||||
return isCloudUiMetadata(value) ? value : undefined;
|
||||
}
|
||||
|
||||
export function getCloudOptionName(option: unknown): unknown {
|
||||
if (typeof option === "object" && option !== null && "name" in option) {
|
||||
return (option as { name?: unknown }).name ?? option;
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
export function isCloudIncompatibleOption(
|
||||
option: unknown,
|
||||
incompatibleOptions: readonly unknown[] = [],
|
||||
): boolean {
|
||||
return incompatibleOptions.some(
|
||||
(incompatibleOption) =>
|
||||
incompatibleOption === getCloudOptionName(option),
|
||||
);
|
||||
}
|
||||
|
||||
export function filterCloudCompatibleOptions<T>(
|
||||
options: T[] | undefined,
|
||||
incompatibleOptions: readonly unknown[] = [],
|
||||
): T[] | undefined {
|
||||
if (!options || incompatibleOptions.length === 0) {
|
||||
return options;
|
||||
}
|
||||
|
||||
return options.filter(
|
||||
(option) => !isCloudIncompatibleOption(option, incompatibleOptions),
|
||||
);
|
||||
}
|
||||
|
||||
export function getCloudFieldOverride(
|
||||
metadata: CloudUiMetadata | undefined,
|
||||
fieldName: string,
|
||||
): CloudFieldOverride | undefined {
|
||||
return metadata?.cloud_default_overrides?.[fieldName];
|
||||
}
|
||||
|
||||
export function getCloudIncompatibleOptions(
|
||||
metadata: CloudUiMetadata | undefined,
|
||||
fieldName: string,
|
||||
): unknown[] {
|
||||
const incompatibleOptions = metadata?.cloud_incompatible_options?.[fieldName];
|
||||
return Array.isArray(incompatibleOptions) ? incompatibleOptions : [];
|
||||
}
|
||||
|
||||
export function applyCloudDefaultOverrides(
|
||||
component: APIClassType,
|
||||
cloudDefaultOverrides?: Record<string, CloudFieldOverride>,
|
||||
): void {
|
||||
if (!cloudDefaultOverrides) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(cloudDefaultOverrides).forEach(([fieldName, override]) => {
|
||||
if (!component.template?.[fieldName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.hasOwn(override, "value")) {
|
||||
component.template[fieldName].value = override.value;
|
||||
}
|
||||
|
||||
if (override.placeholder !== undefined) {
|
||||
component.template[fieldName].placeholder = override.placeholder;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function sanitizeCloudIncompatibleDefaults(
|
||||
component: APIClassType,
|
||||
cloudIncompatibleOptions?: Record<string, unknown[]>,
|
||||
): void {
|
||||
if (!cloudIncompatibleOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(cloudIncompatibleOptions).forEach(
|
||||
([fieldName, incompatibleOptions]) => {
|
||||
if (!Array.isArray(incompatibleOptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const templateField = component.template?.[fieldName];
|
||||
if (!templateField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOptions = Array.isArray(templateField.value)
|
||||
? templateField.value
|
||||
: templateField.value
|
||||
? [templateField.value]
|
||||
: [];
|
||||
|
||||
const filteredSelections = selectedOptions.filter(
|
||||
(selection) =>
|
||||
!isCloudIncompatibleOption(selection, incompatibleOptions),
|
||||
);
|
||||
|
||||
if (filteredSelections.length > 0) {
|
||||
templateField.value = filteredSelections;
|
||||
return;
|
||||
}
|
||||
|
||||
if (templateField.limit !== 1 || !Array.isArray(templateField.options)) {
|
||||
templateField.value = filteredSelections;
|
||||
return;
|
||||
}
|
||||
|
||||
const compatibleOptions = filterCloudCompatibleOptions(
|
||||
templateField.options,
|
||||
incompatibleOptions,
|
||||
);
|
||||
const firstCompatibleOption = compatibleOptions?.[0];
|
||||
|
||||
templateField.value = firstCompatibleOption
|
||||
? [cloneDeep(firstCompatibleOption)]
|
||||
: [];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function withCurrentCloudMetadata(
|
||||
savedNode: APIClassType | undefined,
|
||||
currentCatalogNode: APIClassType | undefined,
|
||||
@ -22,12 +149,8 @@ export function withCurrentCloudMetadata(
|
||||
return savedNode;
|
||||
}
|
||||
|
||||
const savedMetadata = isCloudUiMetadata(savedNode.metadata)
|
||||
? savedNode.metadata
|
||||
: undefined;
|
||||
const currentMetadata = isCloudUiMetadata(currentCatalogNode.metadata)
|
||||
? currentCatalogNode.metadata
|
||||
: undefined;
|
||||
const savedMetadata = getCloudUiMetadata(savedNode.metadata);
|
||||
const currentMetadata = getCloudUiMetadata(currentCatalogNode.metadata);
|
||||
|
||||
const shouldOverlayCloudCompatible =
|
||||
savedNode.cloud_compatible === undefined &&
|
||||
|
||||
@ -73016,7 +73016,7 @@
|
||||
"Message"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "Analyzes a Git repository and returns file contents and complete repository information",
|
||||
@ -73178,7 +73178,7 @@
|
||||
"JSON"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "Load and filter documents from a local or remote Git repository. Use a local repo path or clone from a remote URL.",
|
||||
@ -73775,7 +73775,7 @@
|
||||
"JSON"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "Loads emails from Gmail using provided credentials.",
|
||||
@ -74068,7 +74068,7 @@
|
||||
"JSON"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "Loads documents from Google Drive using provided credentials.",
|
||||
@ -74195,7 +74195,7 @@
|
||||
"Text"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "Searches Google Drive files using provided credentials and query parameters.",
|
||||
@ -74850,7 +74850,7 @@
|
||||
"JSON"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "Generates a JSON string with your Google OAuth token.",
|
||||
@ -75661,7 +75661,7 @@
|
||||
"Tool"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "A very simple tool to control Home Assistant devices. Only action (turn_on, turn_off, toggle) and entity_id need to be provided.",
|
||||
@ -75846,7 +75846,7 @@
|
||||
"Tool"
|
||||
],
|
||||
"beta": false,
|
||||
"cloud_compatible": true,
|
||||
"cloud_compatible": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
"description": "Retrieve states from Home Assistant. The agent only needs to specify 'filter_domain' (optional). Token and base_url are not exposed to the agent.",
|
||||
@ -118918,4 +118918,4 @@
|
||||
},
|
||||
"sha256": "ea05564a639d360e38fa4f323deadb21b41a6b44535408c501ca16048fb54afe",
|
||||
"version": "0.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ class GitLoaderComponent(Component):
|
||||
)
|
||||
trace_type = "tool"
|
||||
icon = "GitLoader"
|
||||
cloud_compatible = False
|
||||
|
||||
inputs = [
|
||||
DropdownInput(
|
||||
|
||||
@ -17,6 +17,7 @@ class GitExtractorComponent(Component):
|
||||
display_name = "GitExtractor"
|
||||
description = "Analyzes a Git repository and returns file contents and complete repository information"
|
||||
icon = "GitLoader"
|
||||
cloud_compatible = False
|
||||
|
||||
inputs = [
|
||||
MessageTextInput(
|
||||
|
||||
@ -26,6 +26,7 @@ class GmailLoaderComponent(Component):
|
||||
icon = "Google"
|
||||
legacy: bool = True
|
||||
replacement = ["composio.ComposioGmailAPIComponent"]
|
||||
cloud_compatible = False
|
||||
|
||||
inputs = [
|
||||
SecretStrInput(
|
||||
|
||||
@ -18,6 +18,7 @@ class GoogleDriveComponent(Component):
|
||||
description = "Loads documents from Google Drive using provided credentials."
|
||||
icon = "Google"
|
||||
legacy: bool = True
|
||||
cloud_compatible = False
|
||||
|
||||
inputs = [
|
||||
SecretStrInput(
|
||||
|
||||
@ -15,6 +15,7 @@ class GoogleDriveSearchComponent(Component):
|
||||
description = "Searches Google Drive files using provided credentials and query parameters."
|
||||
icon = "Google"
|
||||
legacy: bool = True
|
||||
cloud_compatible = False
|
||||
|
||||
inputs = [
|
||||
SecretStrInput(
|
||||
|
||||
@ -18,6 +18,7 @@ class GoogleOAuthToken(Component):
|
||||
icon = "Google"
|
||||
name = "GoogleOAuthToken"
|
||||
legacy: bool = True
|
||||
cloud_compatible = False
|
||||
inputs = [
|
||||
MultilineInput(
|
||||
name="scopes",
|
||||
|
||||
@ -26,6 +26,7 @@ class HomeAssistantControl(LCToolComponent):
|
||||
)
|
||||
documentation: str = "https://developers.home-assistant.io/docs/api/rest/"
|
||||
icon: str = "HomeAssistant"
|
||||
cloud_compatible = False
|
||||
|
||||
# --- Input fields for LangFlow UI (token, URL) ---
|
||||
inputs = [
|
||||
|
||||
@ -20,6 +20,7 @@ class ListHomeAssistantStates(LCToolComponent):
|
||||
)
|
||||
documentation: str = "https://developers.home-assistant.io/docs/api/rest/"
|
||||
icon = "HomeAssistant"
|
||||
cloud_compatible = False
|
||||
|
||||
# 1) Define fields to be received in LangFlow UI
|
||||
inputs = [
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
"""Tests for cloud_compatible component attribute."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from lfx.custom.attributes import ATTR_FUNC_MAPPING, getattr_return_bool
|
||||
from lfx.template.frontend_node.base import FrontendNode
|
||||
|
||||
@ -55,3 +59,48 @@ class TestCloudCompatibleAttribute:
|
||||
assert getattr_return_bool(cloud_false) is False
|
||||
assert getattr_return_bool(None) is None
|
||||
assert getattr_return_bool("not a bool") is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"display_name",
|
||||
[
|
||||
"Git",
|
||||
"GitExtractor",
|
||||
"Gmail Loader",
|
||||
"Google Drive Loader",
|
||||
"Google Drive Search",
|
||||
"Google OAuth Token",
|
||||
"Home Assistant Control",
|
||||
"List Home Assistant States",
|
||||
],
|
||||
)
|
||||
def test_audited_components_are_cloud_incompatible_in_component_index(
|
||||
self,
|
||||
display_name,
|
||||
):
|
||||
"""Audited cloud-hostile components should ship as incompatible in the production index."""
|
||||
component_index_path = (
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "src"
|
||||
/ "lfx"
|
||||
/ "_assets"
|
||||
/ "component_index.json"
|
||||
)
|
||||
|
||||
with component_index_path.open(encoding="utf-8") as index_file:
|
||||
component_index = json.load(index_file)
|
||||
|
||||
matching_component = None
|
||||
for _entry_category, components in component_index["entries"]:
|
||||
matching_component = next(
|
||||
(
|
||||
component
|
||||
for component in components.values()
|
||||
if component.get("display_name") == display_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if matching_component:
|
||||
break
|
||||
|
||||
assert matching_component is not None
|
||||
assert matching_component["cloud_compatible"] is False
|
||||
|
||||
Reference in New Issue
Block a user