diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index a4b86652d3a9..0c4a213e0772 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -45,6 +45,7 @@ get_task_service, get_telemetry_service, ) +from langflow.services.settings.feature_flags import FEATURE_FLAGS from langflow.services.telemetry.schema import RunPayload from langflow.utils.constants import SIDEBAR_CATEGORIES from langflow.utils.version import get_version_info @@ -629,7 +630,8 @@ def get_config(): from langflow.services.deps import get_settings_service settings_service: SettingsService = get_settings_service() - return settings_service.settings.model_dump() + + return {"feature_flags": FEATURE_FLAGS, **settings_service.settings.model_dump()} except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/src/backend/base/langflow/api/v1/schemas.py b/src/backend/base/langflow/api/v1/schemas.py index 2cae487b2885..78e9dc5851b6 100644 --- a/src/backend/base/langflow/api/v1/schemas.py +++ b/src/backend/base/langflow/api/v1/schemas.py @@ -15,6 +15,7 @@ from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.flow import FlowCreate, FlowRead from langflow.services.database.models.user import UserRead +from langflow.services.settings.feature_flags import FeatureFlags from langflow.services.tracing.schema import Log from langflow.utils.util_strings import truncate_long_strings @@ -350,6 +351,7 @@ class FlowDataRequest(BaseModel): class ConfigResponse(BaseModel): + feature_flags: FeatureFlags frontend_timeout: int auto_saving: bool auto_saving_interval: int diff --git a/src/backend/base/langflow/services/settings/feature_flags.py b/src/backend/base/langflow/services/settings/feature_flags.py index 1a8dc0f7a291..6ffd48cf6c9f 100644 --- a/src/backend/base/langflow/services/settings/feature_flags.py +++ b/src/backend/base/langflow/services/settings/feature_flags.py @@ -3,6 +3,7 @@ class FeatureFlags(BaseSettings): add_toolkit_output: bool = False + mvp_components: bool = False class Config: env_prefix = "LANGFLOW_FEATURE_" diff --git a/src/frontend/src/controllers/API/queries/config/use-get-config.ts b/src/frontend/src/controllers/API/queries/config/use-get-config.ts index dad80c811652..b3457b13a9a1 100644 --- a/src/frontend/src/controllers/API/queries/config/use-get-config.ts +++ b/src/frontend/src/controllers/API/queries/config/use-get-config.ts @@ -12,6 +12,7 @@ export interface ConfigResponse { auto_saving_interval: number; health_check_max_retries: number; max_file_size_upload: number; + feature_flags: Record; } export const useGetConfig: useQueryFunctionType = ( @@ -27,6 +28,7 @@ export const useGetConfig: useQueryFunctionType = ( const setMaxFileSizeUpload = useUtilityStore( (state) => state.setMaxFileSizeUpload, ); + const setFeatureFlags = useUtilityStore((state) => state.setFeatureFlags); const { query } = UseRequestProcessor(); @@ -43,6 +45,7 @@ export const useGetConfig: useQueryFunctionType = ( setAutoSavingInterval(data.auto_saving_interval); setHealthCheckMaxRetries(data.health_check_max_retries); setMaxFileSizeUpload(data.max_file_size_upload); + setFeatureFlags(data.feature_flags); } return data; }; diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 6e05568ed1dc..993bd72f1ca9 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -20,6 +20,7 @@ import { nodeIconsLucide } from "../../../../utils/styleUtils"; import ParentDisclosureComponent from "../ParentDisclosureComponent"; import { SidebarCategoryComponent } from "./SidebarCategoryComponent"; +import { useUtilityStore } from "@/stores/utilityStore"; import { SidebarFilterComponent } from "./sidebarFilterComponent"; import { sortKeys } from "./utils"; @@ -31,6 +32,8 @@ export default function ExtraSidebar(): JSX.Element { const hasStore = useStoreStore((state) => state.hasStore); const filterType = useFlowStore((state) => state.filterType); + const featureFlags = useUtilityStore((state) => state.featureFlags); + const setErrorData = useAlertStore((state) => state.setErrorData); const [dataFilter, setFilterData] = useState(data); const [search, setSearch] = useState(""); @@ -248,7 +251,7 @@ export default function ExtraSidebar(): JSX.Element {
), )} - {ENABLE_INTEGRATIONS && ( + {(ENABLE_INTEGRATIONS || featureFlags?.mvp_components) && ( ((set, get) => ({ setFlowsPagination: (flowsPagination: Pagination) => set({ flowsPagination }), tags: [], setTags: (tags: Tag[]) => set({ tags }), + featureFlags: {}, + setFeatureFlags: (featureFlags: Record) => set({ featureFlags }), })); diff --git a/src/frontend/src/types/zustand/utility/index.ts b/src/frontend/src/types/zustand/utility/index.ts index 76d0c6452dac..504fa8fac85b 100644 --- a/src/frontend/src/types/zustand/utility/index.ts +++ b/src/frontend/src/types/zustand/utility/index.ts @@ -13,4 +13,6 @@ export type UtilityStoreType = { setFlowsPagination: (pagination: Pagination) => void; tags: Tag[]; setTags: (tags: Tag[]) => void; + featureFlags: Record; + setFeatureFlags: (featureFlags: Record) => void; }; diff --git a/src/frontend/tests/core/features/stop-building.spec.ts b/src/frontend/tests/core/features/stop-building.spec.ts index d79d85c6ec12..964e2953778e 100644 --- a/src/frontend/tests/core/features/stop-building.spec.ts +++ b/src/frontend/tests/core/features/stop-building.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "@playwright/test"; +import uaParser from "ua-parser-js"; test("user must be able to stop a building", async ({ page }) => { await page.goto("/"); @@ -20,6 +21,14 @@ test("user must be able to stop a building", async ({ page }) => { modalCount = await page.getByTestId("modal-title")?.count(); } + const getUA = await page.evaluate(() => navigator.userAgent); + const userAgentInfo = uaParser(getUA); + let control = "Control"; + + if (userAgentInfo.os.name.includes("Mac")) { + control = "Meta"; + } + await page.getByTestId("blank-flow").click(); //first component @@ -203,7 +212,62 @@ test("user must be able to stop a building", async ({ page }) => { await page.getByTestId("int_int_chunk_size").fill("2"); await page.getByTestId("int_int_chunk_overlap").fill("1"); - await page.getByTestId("button_run_chat output").click(); + const timerCode = ` +# from langflow.field_typing import Data +from langflow.custom import Component +from langflow.io import MessageTextInput, Output +from langflow.schema import Data +import time + +class CustomComponent(Component): + display_name = "Custom Component" + description = "Use as a template to create your own component." + documentation: str = "http://docs.langflow.org/components/custom" + icon = "custom_components" + name = "CustomComponent" + + inputs = [ + MessageTextInput(name="input_value", display_name="Input Value", value="Hello, World!"), + ] + + outputs = [ + Output(display_name="Output", name="output", method="build_output"), + ] + + def build_output(self) -> Data: + time.sleep(10000) + data = Data(value=self.input_value) + self.status = data + return data + `; + + await page.getByTestId("extended-disclosure").click(); + await page.getByPlaceholder("Search").click(); + await page.getByPlaceholder("Search").fill("custom component"); + + await page.waitForTimeout(1000); + + await page + .locator('//*[@id="helpersCustom Component"]') + .dragTo(page.locator('//*[@id="react-flow-id"]')); + await page.mouse.up(); + await page.mouse.down(); + await page.getByTitle("fit view").click(); + await page.getByTitle("zoom out").click(); + + await page.getByTestId("title-Custom Component").first().click(); + + await page.waitForTimeout(500); + await page.getByTestId("code-button-modal").click(); + await page.waitForTimeout(500); + + await page.locator("textarea").last().press(`${control}+a`); + await page.keyboard.press("Backspace"); + await page.locator("textarea").last().fill(timerCode); + await page.locator('//*[@id="checkAndSaveBtn"]').click(); + await page.waitForTimeout(500); + + await page.getByTestId("button_run_custom component").click(); await page.waitForSelector("text=Building", { timeout: 100000, @@ -233,7 +297,7 @@ test("user must be able to stop a building", async ({ page }) => { timeout: 100000, }); - await page.getByTestId("button_run_chat output").click(); + await page.getByTestId("button_run_custom component").click(); await page.waitForSelector("text=Building", { timeout: 100000, @@ -269,7 +333,7 @@ test("user must be able to stop a building", async ({ page }) => { timeout: 100000, }); - await page.getByTestId("button_run_chat output").click(); + await page.getByTestId("button_run_custom component").click(); await page.waitForSelector('[data-testid="loading_icon"]', { timeout: 100000, diff --git a/src/frontend/tests/extended/features/integration-side-bar.spec.ts b/src/frontend/tests/extended/features/integration-side-bar.spec.ts new file mode 100644 index 000000000000..99f64efde4aa --- /dev/null +++ b/src/frontend/tests/extended/features/integration-side-bar.spec.ts @@ -0,0 +1,96 @@ +import { expect, test } from "@playwright/test"; + +test("user should be able to see integrations in the sidebar if mvp_components is true", async ({ + page, +}) => { + await page.route("**/api/v1/config", (route) => { + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + feature_flags: { + mvp_components: true, + }, + }), + headers: { + "content-type": "application/json", + ...route.request().headers(), + }, + }); + }); + + await page.goto("/"); + await page.waitForTimeout(1000); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + while (modalCount === 0) { + await page.getByText("New Project", { exact: true }).click(); + await page.waitForTimeout(3000); + modalCount = await page.getByTestId("modal-title")?.count(); + } + + await page.getByTestId("blank-flow").click(); + await page.waitForSelector('[data-testid="extended-disclosure"]', { + timeout: 30000, + }); + + await expect(page.getByText("Integrations")).toBeVisible(); + await expect(page.getByText("Notion")).toBeVisible(); +}); + +test("user should NOT be able to see integrations in the sidebar if mvp_components is false", async ({ + page, +}) => { + await page.waitForTimeout(4000); + await page.route("**/api/v1/config", (route) => { + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + feature_flags: { + mvp_components: false, + }, + }), + headers: { + "content-type": "application/json", + ...route.request().headers(), + }, + }); + }); + + await page.goto("/"); + await page.waitForTimeout(1000); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + while (modalCount === 0) { + await page.getByText("New Project", { exact: true }).click(); + await page.waitForTimeout(3000); + modalCount = await page.getByTestId("modal-title")?.count(); + } + + await page.getByTestId("blank-flow").click(); + await page.waitForSelector('[data-testid="extended-disclosure"]', { + timeout: 30000, + }); + + await expect(page.getByText("Integrations")).not.toBeVisible(); + await expect(page.getByText("Notion")).not.toBeVisible(); +});