feat: add observable UX for community interaction tracking (#7512)
* ✨ (model.py): add UserOptin class to manage user opt-in actions for better organization and extensibility ♻️ (model.py): refactor User model to include user_optin field as a dictionary to store opt-in actions for users * [autofix.ci] apply automated fixes * change name optins * [autofix.ci] apply automated fixes * ✨ (add_optins_column_to_user.py): Add optins column to the user table to store user preferences ♻️ (model.py): Refactor UserOptin class to BaseModel for better type hinting and add optins field to User model with default values and proper typing * [autofix.ci] apply automated fixes * 🐛 (add_optins_column_to_user.py): fix an issue where the optins column was not being added if it already existed in the user table * ✨ (empty-page.tsx): Add new page EmptyPageCommunity to display community information and actions ✨ (main-page.tsx): Add new page CollectionPage to manage collections and folders 🔧 (routes.tsx): Update import path for CollectionPage to point to the new main-page file * ✨ (background-gradient.tsx): Add a new component BackgroundGradient to create a visually appealing background gradient effect for UI elements 📝 (empty-page.tsx): Refactor EmptyPageCommunity component to use the newly added BackgroundGradient component for GitHub and Discord sections to enhance visual appeal and consistency * 📝 (tasks.mdc): Add concise task management protocol for sequential mode to improve task organization and execution ✨ (frontend): Introduce DotBackgroundDemo component for creating a visually appealing dot background effect ♻️ (frontend): Refactor BackgroundGradient component to improve gradient styling and border consistency 🔧 (frontend): Update motion import in background-gradient.tsx to use framer-motion instead of motion/react 🔧 (icons): Add missing newline at the end of Anthropic icon file 🔧 (empty-page.tsx): Adjust styling classes and z-index to improve layout and visual hierarchy in EmptyPageCommunity component * ✨ (empty-page.tsx): add githubBg image import to use as background image for GitHub link ♻️ (empty-page.tsx): refactor positioning and styling of GitHub link elements for better alignment and readability * 🔧 refactor(server.ts): change port variable case from lowercase port to uppercase PORT to improve semantics ✨ refactor(server.ts): add support for process.env.PORT environment variable to be able to run app on a configurable port * 📝 (AccountMenu/index.tsx): Update imports and remove unused code for better organization and performance 🔧 (use-get-version.ts): Add functionality to refresh the latest version in darkStore after fetching version data ♻️ (darkStore.ts): Add refreshLatestVersion function to update the latest version in darkStore 📝 (dark/index.ts): Add latestVersion field and refreshLatestVersion function to DarkStoreType for better state management * ✨ (AccountMenu/index.tsx): Add constants for Discord, Docs, GitHub, and Twitter URLs for better maintainability and reusability 📝 (constants.ts): Update Twitter URL to a new value for consistency with other URLs 📝 (TwitterX): Add new TwitterX icon and component for use in the application 📝 (styleUtils.ts): Import and use the new TwitterXIcon in the list of node icons 📝 (utils.ts): Update formatNumber function to handle undefined input values for better error handling * [autofix.ci] apply automated fixes * 📝 (add_optins_column_to_user.py): Update down_revision to '1b8b740a6fa3' for consistency 🔧 (AccountMenu/index.tsx): Adjust classNameSize prop value to 'w-[272px]' for styling consistency 🔧 (HeaderMenu/index.tsx): Update HeaderMenuItems component to accept classNameSize prop for dynamic styling 🔧 (langflow-counts.tsx): Adjust styling for better visual consistency and spacing 🔧 (index.tsx): Update className for Bell icon to include text-muted-foreground and strokeWidth 🔧 (get-started-progress.tsx): Update styling and spacing for better visual consistency 🔧 (header-buttons.tsx): Add Separator component for visual separation in HeaderButtons component * 🔧 (AccountMenu/index.tsx): Adjust padding in AccountMenu component for better alignment and spacing. Fix ThemeButtons positioning for improved layout. * ✨ (appHeaderComponent/index.tsx): Add support for managing flows and folders in the app header component 📝 (get-started-progress.tsx): Update heading tag to improve semantics 📝 (empty-page.tsx): Update text content in empty page to provide clearer instructions and information * ✨ (background-gradient.tsx): Add support for dynamic border radius in BackgroundGradient component 🔧 (empty-page.tsx): Remove BackgroundGradient import and replace it with EnhancedBeamEffect component 🔧 (empty-page.tsx): Update styles and classes for EnhancedBeamEffect component and adjust layout ✨ (enchanced-beam-effect.tsx): Create EnhancedBeamEffect component to add enhanced beam effect to UI components * ✨ (frontend): update text content and button labels in empty page component for better user experience 📝 (frontend): add data-testid attributes for testing purposes in various components 🔧 (frontend): add new test file for user progress tracking feature with Playwright tests * ✨ (AccountMenu/index.tsx): Add Admin Page button for admin users in the account menu component 🔧 (user-progress-track.spec.ts): Add utility function addNewUserAndLogin to facilitate adding and logging in new users for testing purposes * [autofix.ci] apply automated fixes * 🐛 (get-started-progress.tsx): fix calculation of percentage to ensure it does not exceed 100% * ✨ (empty-page.tsx): Add Lucide ExternalLink component for external links and update styling for external link icons ♻️ (empty-page.tsx): Refactor CSS classes for external link icons to improve readability and maintainability 📝 (index.css): Add custom CSS variable for Discord color 📝 (tailwind.config.mjs): Add Discord color to Tailwind CSS custom colors * [autofix.ci] apply automated fixes * add logo png * ✨ (index.tsx): Add z-50 class to improve stacking context in CardsWrapComponent ✨ (empty-page.tsx): Add text-center class to center text elements in EmptyPageCommunity ✨ (empty-page.tsx): Adjust spacing and alignment in EmptyPageCommunity for better layout and readability * 🐛 (AccountMenu/index.tsx): fix condition to show admin options only when isAdmin is true and autoLogin is false * 🔧 (alertDropDown/index.tsx): update z-index value in PopoverContent class to z-50 for proper stacking order * 🔧 (index.tsx): update z-index value to improve the stacking order of the component on the page * ♻️ (index.tsx): refactor classNames in CardsWrapComponent to improve readability and maintainability * 🐛 (empty-page.tsx): fix data-testid attribute value to match the updated element name for better consistency and clarity 🐛 (user-progress-track.spec.ts): fix test cases to match the updated data-testid attribute value for the main page title element to ensure accurate testing and assertions --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
e30b1985d1
commit
a8ae17b86d
45 changed files with 1482 additions and 209 deletions
51
.cursor/rules/tasks.mdc
Normal file
51
.cursor/rules/tasks.mdc
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# Concise Task Management Protocol - Sequential Mode
|
||||||
|
|
||||||
|
## File Creation
|
||||||
|
- Filename: `YYYYMMDD_HHMMSS_task_name.md`
|
||||||
|
- IMPORTANT: Always include a descriptive task name after the timestamp (e.g., `20250404_135621_api_integration.md`)
|
||||||
|
- Never create files with timestamp only
|
||||||
|
- Store in `tasks/` directory
|
||||||
|
|
||||||
|
## Task Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
# Task: [Task Name]
|
||||||
|
**Status**: [Not Started | In Progress | Completed]
|
||||||
|
|
||||||
|
## Analysis
|
||||||
|
- [ ] Requirements
|
||||||
|
- [ ] Subtask 1
|
||||||
|
- [ ] Challenges
|
||||||
|
- [ ] Dependencies
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
- [ ] Step 1: [Description]
|
||||||
|
- [ ] Subtask 1.1
|
||||||
|
- [ ] Step 2: [Description]
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
- [ ] Implementation 1
|
||||||
|
- [ ] Details
|
||||||
|
- [ ] Implementation 2
|
||||||
|
- [ ] Files modified: [files]
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- [ ] Files modified: `filename.ext` (lines X-Y)
|
||||||
|
- [ ] Dependencies added/changed
|
||||||
|
- [ ] Edge cases considered
|
||||||
|
- [ ] Known limitations
|
||||||
|
- [ ] Future impact points
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execution Rules
|
||||||
|
- Execute one subtask at a time in sequence
|
||||||
|
- Update the task file after EACH subtask is completed
|
||||||
|
- Mark completed subtasks with [x] as they are finished
|
||||||
|
- Update main task status throughout execution
|
||||||
|
- Document work incrementally, not all at once
|
||||||
|
- Never proceed to the next section until all subtasks in the current section are completed and marked
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""add_optins_column_to_user
|
||||||
|
|
||||||
|
Revision ID: e56d87f8994a
|
||||||
|
Revises: 1b8b740a6fa3
|
||||||
|
Create Date: 2025-04-09 15:57:46.904977
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel
|
||||||
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
|
from langflow.utils import migration
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'e56d87f8994a'
|
||||||
|
down_revision: Union[str, None] = '1b8b740a6fa3'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
if not migration.column_exists(table_name='user', column_name='optins', conn=conn):
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('optins', sa.JSON(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('optins')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import JSON, Column
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
from langflow.schema.serialize import UUIDstr
|
from langflow.schema.serialize import UUIDstr
|
||||||
|
|
@ -13,6 +15,13 @@ if TYPE_CHECKING:
|
||||||
from langflow.services.database.models.variable import Variable
|
from langflow.services.database.models.variable import Variable
|
||||||
|
|
||||||
|
|
||||||
|
class UserOptin(BaseModel):
|
||||||
|
github_starred: bool = Field(default=False)
|
||||||
|
dialog_dismissed: bool = Field(default=False)
|
||||||
|
discord_clicked: bool = Field(default=False)
|
||||||
|
# Add more opt-in actions as needed
|
||||||
|
|
||||||
|
|
||||||
class User(SQLModel, table=True): # type: ignore[call-arg]
|
class User(SQLModel, table=True): # type: ignore[call-arg]
|
||||||
id: UUIDstr = Field(default_factory=uuid4, primary_key=True, unique=True)
|
id: UUIDstr = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||||
username: str = Field(index=True, unique=True)
|
username: str = Field(index=True, unique=True)
|
||||||
|
|
@ -37,11 +46,17 @@ class User(SQLModel, table=True): # type: ignore[call-arg]
|
||||||
back_populates="user",
|
back_populates="user",
|
||||||
sa_relationship_kwargs={"cascade": "delete"},
|
sa_relationship_kwargs={"cascade": "delete"},
|
||||||
)
|
)
|
||||||
|
optins: dict[str, Any] | None = Field(
|
||||||
|
sa_column=Column(JSON, default=lambda: UserOptin().model_dump(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(SQLModel):
|
class UserCreate(SQLModel):
|
||||||
username: str = Field()
|
username: str = Field()
|
||||||
password: str = Field()
|
password: str = Field()
|
||||||
|
optins: dict[str, Any] | None = Field(
|
||||||
|
default={"github_starred": False, "dialog_dismissed": False, "discord_clicked": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserRead(SQLModel):
|
class UserRead(SQLModel):
|
||||||
|
|
@ -54,6 +69,7 @@ class UserRead(SQLModel):
|
||||||
create_at: datetime = Field()
|
create_at: datetime = Field()
|
||||||
updated_at: datetime = Field()
|
updated_at: datetime = Field()
|
||||||
last_login_at: datetime | None = Field(nullable=True)
|
last_login_at: datetime | None = Field(nullable=True)
|
||||||
|
optins: dict[str, Any] | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(SQLModel):
|
class UserUpdate(SQLModel):
|
||||||
|
|
@ -63,3 +79,4 @@ class UserUpdate(SQLModel):
|
||||||
is_active: bool | None = None
|
is_active: bool | None = None
|
||||||
is_superuser: bool | None = None
|
is_superuser: bool | None = None
|
||||||
last_login_at: datetime | None = None
|
last_login_at: datetime | None = None
|
||||||
|
optins: dict[str, Any] | None = None
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,6 @@ const RenderInputParameters = ({
|
||||||
return keyMap;
|
return keyMap;
|
||||||
}, [templateFields, data.id, data.node?.template]);
|
}, [templateFields, data.id, data.node?.template]);
|
||||||
|
|
||||||
console.log({ data });
|
|
||||||
|
|
||||||
const renderInputParameter = templateFields.map(
|
const renderInputParameter = templateFields.map(
|
||||||
(templateField: string, idx) => {
|
(templateField: string, idx) => {
|
||||||
const template = data.node?.template[templateField];
|
const template = data.node?.template[templateField];
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const AlertDropdown = forwardRef<HTMLDivElement, AlertDropdownType>(
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
ref={notificationRef}
|
ref={notificationRef}
|
||||||
data-testid="notification-dropdown-content"
|
data-testid="notification-dropdown-content"
|
||||||
className="noflow nowheel nopan nodelete nodrag z-10 flex h-[500px] w-[500px] flex-col"
|
className="noflow nowheel nopan nodelete nodrag z-50 flex h-[500px] w-[500px] flex-col"
|
||||||
>
|
>
|
||||||
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
|
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
|
||||||
Notifications
|
Notifications
|
||||||
|
|
|
||||||
BIN
src/frontend/src/assets/github-bg.png
Normal file
BIN
src/frontend/src/assets/github-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/frontend/src/assets/logo_dark.png
Normal file
BIN
src/frontend/src/assets/logo_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
src/frontend/src/assets/logo_light.png
Normal file
BIN
src/frontend/src/assets/logo_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -1,33 +1,31 @@
|
||||||
|
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
|
||||||
|
import {
|
||||||
|
DISCORD_URL,
|
||||||
|
DOCS_URL,
|
||||||
|
GITHUB_URL,
|
||||||
|
TWITTER_URL,
|
||||||
|
} from "@/constants/constants";
|
||||||
import { useLogout } from "@/controllers/API/queries/auth";
|
import { useLogout } from "@/controllers/API/queries/auth";
|
||||||
import CustomFeatureFlagDialog from "@/customization/components/custom-feature-flag-dialog";
|
|
||||||
import CustomFeatureFlagMenuItems from "@/customization/components/custom-feature-flag-menu-items";
|
|
||||||
import { CustomFeedbackDialog } from "@/customization/components/custom-feedback-dialog";
|
|
||||||
import { CustomHeaderMenuItemsTitle } from "@/customization/components/custom-header-menu-items-title";
|
|
||||||
import { CustomProfileIcon } from "@/customization/components/custom-profile-icon";
|
|
||||||
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
|
||||||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||||
import useAuthStore from "@/stores/authStore";
|
import useAuthStore from "@/stores/authStore";
|
||||||
import { useDarkStore } from "@/stores/darkStore";
|
import { useDarkStore } from "@/stores/darkStore";
|
||||||
import { useState } from "react";
|
import { cn } from "@/utils/utils";
|
||||||
|
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import GithubStarComponent from "../GithubStarButton";
|
|
||||||
import {
|
import {
|
||||||
HeaderMenu,
|
HeaderMenu,
|
||||||
HeaderMenuItemButton,
|
HeaderMenuItemButton,
|
||||||
HeaderMenuItemLink,
|
HeaderMenuItemLink,
|
||||||
HeaderMenuItems,
|
HeaderMenuItems,
|
||||||
HeaderMenuItemsSection,
|
|
||||||
HeaderMenuToggle,
|
HeaderMenuToggle,
|
||||||
} from "../HeaderMenu";
|
} from "../HeaderMenu";
|
||||||
import { ProfileIcon } from "../ProfileIcon";
|
import { ProfileIcon } from "../ProfileIcon";
|
||||||
import ThemeButtons from "../ThemeButtons";
|
import ThemeButtons from "../ThemeButtons";
|
||||||
|
|
||||||
export const AccountMenu = () => {
|
export const AccountMenu = () => {
|
||||||
const [isFeedbackOpen, setIsFeedbackOpen] = useState(false);
|
|
||||||
const [isCustomFeatureFlagsOpen, setIsCustomFeatureFlagsOpen] =
|
|
||||||
useState(false);
|
|
||||||
const { customParam: id } = useParams();
|
const { customParam: id } = useParams();
|
||||||
const version = useDarkStore((state) => state.version);
|
const version = useDarkStore((state) => state.version);
|
||||||
|
const latestVersion = useDarkStore((state) => state.latestVersion);
|
||||||
const navigate = useCustomNavigate();
|
const navigate = useCustomNavigate();
|
||||||
const { mutate: mutationLogout } = useLogout();
|
const { mutate: mutationLogout } = useLogout();
|
||||||
|
|
||||||
|
|
@ -40,6 +38,8 @@ export const AccountMenu = () => {
|
||||||
mutationLogout();
|
mutationLogout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isLatestVersion = version === latestVersion;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderMenu>
|
<HeaderMenu>
|
||||||
|
|
@ -48,34 +48,37 @@ export const AccountMenu = () => {
|
||||||
className="h-7 w-7 rounded-lg focus-visible:outline-0"
|
className="h-7 w-7 rounded-lg focus-visible:outline-0"
|
||||||
data-testid="user-profile-settings"
|
data-testid="user-profile-settings"
|
||||||
>
|
>
|
||||||
{ENABLE_DATASTAX_LANGFLOW ? <CustomProfileIcon /> : <ProfileIcon />}
|
<ProfileIcon />
|
||||||
</div>
|
</div>
|
||||||
</HeaderMenuToggle>
|
</HeaderMenuToggle>
|
||||||
<HeaderMenuItems position="right">
|
<HeaderMenuItems position="right" classNameSize="w-[272px]">
|
||||||
{ENABLE_DATASTAX_LANGFLOW && (
|
<div className="divide-y divide-foreground/10">
|
||||||
<HeaderMenuItemsSection>
|
<div>
|
||||||
<CustomHeaderMenuItemsTitle />
|
<div className="h-[46px] items-center px-4 pt-3">
|
||||||
</HeaderMenuItemsSection>
|
<div className="flex items-center justify-between">
|
||||||
)}
|
<span
|
||||||
<HeaderMenuItemsSection>
|
data-testid="menu_version_button"
|
||||||
<div className="flex h-[46px] w-full items-center justify-between px-3">
|
id="menu_version_button"
|
||||||
<div className="pl-1 text-xs text-zinc-500">
|
className="text-sm"
|
||||||
<span
|
>
|
||||||
data-testid="menu_version_button"
|
Version
|
||||||
id="menu_version_button"
|
</span>
|
||||||
>
|
<div
|
||||||
Version {version}
|
className={cn(
|
||||||
</span>
|
"float-right text-xs",
|
||||||
|
isLatestVersion && "text-accent-emerald-foreground",
|
||||||
|
!isLatestVersion && "text-accent-amber-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{version}{" "}
|
||||||
|
{isLatestVersion ? "(latest)" : "(update available)"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!ENABLE_DATASTAX_LANGFLOW && <ThemeButtons />}
|
|
||||||
</div>
|
</div>
|
||||||
{ENABLE_DATASTAX_LANGFLOW ? (
|
|
||||||
<HeaderMenuItemLink newPage href={`/settings/org/${id}/overview`}>
|
<div>
|
||||||
Account Settings
|
|
||||||
</HeaderMenuItemLink>
|
|
||||||
) : (
|
|
||||||
<HeaderMenuItemButton
|
<HeaderMenuItemButton
|
||||||
icon="arrow-right"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/settings");
|
navigate("/settings");
|
||||||
}}
|
}}
|
||||||
|
|
@ -87,100 +90,84 @@ export const AccountMenu = () => {
|
||||||
Settings
|
Settings
|
||||||
</span>
|
</span>
|
||||||
</HeaderMenuItemButton>
|
</HeaderMenuItemButton>
|
||||||
)}
|
|
||||||
{!ENABLE_DATASTAX_LANGFLOW && (
|
{isAdmin && !autoLogin && (
|
||||||
<>
|
<div>
|
||||||
{isAdmin && !autoLogin && (
|
<HeaderMenuItemButton
|
||||||
<HeaderMenuItemButton onClick={() => navigate("/admin")}>
|
onClick={() => {
|
||||||
|
navigate("/admin");
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
data-testid="menu_admin_button"
|
data-testid="menu_admin_page_button"
|
||||||
id="menu_admin_button"
|
id="menu_admin_page_button"
|
||||||
>
|
>
|
||||||
Admin Page
|
Admin Page
|
||||||
</span>
|
</span>
|
||||||
</HeaderMenuItemButton>
|
</HeaderMenuItemButton>
|
||||||
)}
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
<HeaderMenuItemLink newPage href={DOCS_URL}>
|
||||||
{ENABLE_DATASTAX_LANGFLOW ? (
|
|
||||||
<>
|
|
||||||
<HeaderMenuItemButton onClick={() => setIsFeedbackOpen(true)}>
|
|
||||||
<span
|
|
||||||
data-testid="menu_feedback_button"
|
|
||||||
id="menu_feedback_button"
|
|
||||||
>
|
|
||||||
Feedback
|
|
||||||
</span>
|
|
||||||
</HeaderMenuItemButton>
|
|
||||||
<CustomFeatureFlagMenuItems
|
|
||||||
onClick={() => setIsCustomFeatureFlagsOpen(true)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<HeaderMenuItemLink newPage href="https://docs.langflow.org">
|
|
||||||
<span data-testid="menu_docs_button" id="menu_docs_button">
|
<span data-testid="menu_docs_button" id="menu_docs_button">
|
||||||
Docs
|
Docs
|
||||||
</span>
|
</span>
|
||||||
</HeaderMenuItemLink>
|
</HeaderMenuItemLink>
|
||||||
)}
|
</div>
|
||||||
</HeaderMenuItemsSection>
|
|
||||||
<HeaderMenuItemsSection>
|
<div>
|
||||||
{ENABLE_DATASTAX_LANGFLOW ? (
|
<HeaderMenuItemLink newPage href={GITHUB_URL}>
|
||||||
<HeaderMenuItemLink
|
<span
|
||||||
newPage
|
data-testid="menu_github_button"
|
||||||
href="https://github.com/langflow-ai/langflow"
|
id="menu_github_button"
|
||||||
>
|
className="flex items-center gap-2"
|
||||||
<div className="-my-2 mr-2 flex w-full items-center justify-between">
|
>
|
||||||
<div className="text-sm">Star the repo</div>
|
<FaGithub className="h-4 w-4" />
|
||||||
<GithubStarComponent />
|
GitHub
|
||||||
</div>
|
|
||||||
</HeaderMenuItemLink>
|
|
||||||
) : (
|
|
||||||
<HeaderMenuItemLink
|
|
||||||
newPage
|
|
||||||
href="https://github.com/langflow-ai/langflow/discussions"
|
|
||||||
>
|
|
||||||
<span data-testid="menu_github_button" id="menu_github_button">
|
|
||||||
Share Feedback on Github
|
|
||||||
</span>
|
</span>
|
||||||
</HeaderMenuItemLink>
|
</HeaderMenuItemLink>
|
||||||
)}
|
<HeaderMenuItemLink newPage href={DISCORD_URL}>
|
||||||
<HeaderMenuItemLink newPage href="https://twitter.com/langflow_ai">
|
<span
|
||||||
<span data-testid="menu_twitter_button" id="menu_twitter_button">
|
data-testid="menu_discord_button"
|
||||||
Follow Langflow on X
|
id="menu_discord_button"
|
||||||
</span>
|
className="flex items-center gap-2"
|
||||||
</HeaderMenuItemLink>
|
>
|
||||||
<HeaderMenuItemLink newPage href="https://discord.gg/EqksyE2EX9">
|
<FaDiscord className="h-4 w-4 text-[#5865F2]" />
|
||||||
<span data-testid="menu_discord_button" id="menu_discord_button">
|
Discord
|
||||||
Join the Langflow Discord
|
</span>
|
||||||
</span>
|
|
||||||
</HeaderMenuItemLink>
|
|
||||||
</HeaderMenuItemsSection>
|
|
||||||
{ENABLE_DATASTAX_LANGFLOW ? (
|
|
||||||
<HeaderMenuItemsSection>
|
|
||||||
<HeaderMenuItemLink href="/session/logout" icon="log-out">
|
|
||||||
Logout
|
|
||||||
</HeaderMenuItemLink>
|
</HeaderMenuItemLink>
|
||||||
</HeaderMenuItemsSection>
|
<HeaderMenuItemLink newPage href={TWITTER_URL}>
|
||||||
) : (
|
<span
|
||||||
!autoLogin && (
|
data-testid="menu_twitter_button"
|
||||||
<HeaderMenuItemsSection>
|
id="menu_twitter_button"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<ForwardedIconComponent
|
||||||
|
strokeWidth={2}
|
||||||
|
name="TwitterXIcon"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
X
|
||||||
|
</span>
|
||||||
|
</HeaderMenuItemLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between px-4 py-1 text-sm">
|
||||||
|
<span className="">Theme</span>
|
||||||
|
<div className="relative top-[1px] float-right">
|
||||||
|
<ThemeButtons />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!autoLogin && (
|
||||||
|
<div>
|
||||||
<HeaderMenuItemButton onClick={handleLogout} icon="log-out">
|
<HeaderMenuItemButton onClick={handleLogout} icon="log-out">
|
||||||
Logout
|
Logout
|
||||||
</HeaderMenuItemButton>
|
</HeaderMenuItemButton>
|
||||||
</HeaderMenuItemsSection>
|
</div>
|
||||||
)
|
)}
|
||||||
)}
|
</div>
|
||||||
</HeaderMenuItems>
|
</HeaderMenuItems>
|
||||||
</HeaderMenu>
|
</HeaderMenu>
|
||||||
<CustomFeedbackDialog
|
|
||||||
isOpen={isFeedbackOpen}
|
|
||||||
setIsOpen={setIsFeedbackOpen}
|
|
||||||
/>
|
|
||||||
<CustomFeatureFlagDialog
|
|
||||||
isOpen={isCustomFeatureFlagsOpen}
|
|
||||||
setIsOpen={setIsCustomFeatureFlagsOpen}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
|
||||||
import { useDarkStore } from "@/stores/darkStore";
|
|
||||||
import { FaGithub } from "react-icons/fa";
|
|
||||||
|
|
||||||
export const GithubStarComponent = () => {
|
|
||||||
const stars: number | undefined = useDarkStore((state) => state.stars);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ShadTooltip content="Go to Github repo" side="bottom" styleClasses="z-10">
|
|
||||||
<div className="group inline-flex h-8 items-center justify-center gap-1 rounded-md border bg-muted px-2 pr-0 hover:border-input hover:bg-secondary-hover">
|
|
||||||
<FaGithub className="h-4 w-4" />
|
|
||||||
<div className="hidden text-xs font-semibold lg:block">Star</div>
|
|
||||||
<div className="-mr-px ml-1 flex h-8 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-xs font-semibold text-secondary-foreground group-hover:border-input">
|
|
||||||
{stars?.toLocaleString() ?? 0}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ShadTooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GithubStarComponent;
|
|
||||||
|
|
@ -22,11 +22,7 @@ export const HeaderMenuToggle = ({ children }) => (
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 rounded-lg px-2 py-1.5 group-hover:bg-muted">
|
<div className="flex items-center gap-1 rounded-lg px-2 py-1.5 group-hover:bg-muted">
|
||||||
{children}
|
{children}
|
||||||
<ChevronsUpDown
|
<ChevronsUpDown className="h-4 w-4 text-muted-foreground group-hover:text-foreground" />
|
||||||
className="text-muted-foreground group-hover:text-foreground"
|
|
||||||
size={"15px"}
|
|
||||||
strokeWidth={"2px"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
);
|
);
|
||||||
|
|
@ -72,10 +68,14 @@ export const HeaderMenuItemButton = ({ icon = "", onClick, children }) => (
|
||||||
export const HeaderMenuItems = ({
|
export const HeaderMenuItems = ({
|
||||||
position = "left",
|
position = "left",
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{ position?: "left" | "right" }>) => {
|
classNameSize = "w-[20rem]",
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
position?: "left" | "right";
|
||||||
|
classNameSize?: string;
|
||||||
|
}>) => {
|
||||||
const positionClass = position === "left" ? "left-0" : "right-0";
|
const positionClass = position === "left" ? "left-0" : "right-0";
|
||||||
return (
|
return (
|
||||||
<DropdownMenuContent className={cn("w-[20rem]", positionClass)}>
|
<DropdownMenuContent className={cn(classNameSize, positionClass)}>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export const ThemeButtons = () => {
|
||||||
data-testid="menu_light_button"
|
data-testid="menu_light_button"
|
||||||
id="menu_light_button"
|
id="menu_light_button"
|
||||||
>
|
>
|
||||||
<ForwardedIconComponent name="Sun" className="w-4" />
|
<ForwardedIconComponent strokeWidth={2} name="Sun" className="w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Dark Theme Button */}
|
{/* Dark Theme Button */}
|
||||||
|
|
@ -68,14 +68,14 @@ export const ThemeButtons = () => {
|
||||||
unstyled
|
unstyled
|
||||||
className={`relative z-10 mx-1 inline-flex items-center rounded-full px-1 ${
|
className={`relative z-10 mx-1 inline-flex items-center rounded-full px-1 ${
|
||||||
selectedTheme === "dark"
|
selectedTheme === "dark"
|
||||||
? "text-background dark:hover:bg-purple-400"
|
? "bg-indigo-foreground text-primary hover:bg-indigo-foreground"
|
||||||
: "text-foreground hover:bg-purple-400 hover:text-background"
|
: "text-foreground hover:bg-indigo-foreground hover:text-background"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleThemeChange("dark")}
|
onClick={() => handleThemeChange("dark")}
|
||||||
data-testid="menu_dark_button"
|
data-testid="menu_dark_button"
|
||||||
id="menu_dark_button"
|
id="menu_dark_button"
|
||||||
>
|
>
|
||||||
<ForwardedIconComponent name="Moon" className="w-4" />
|
<ForwardedIconComponent strokeWidth={2} name="Moon" className="w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* System Theme Button */}
|
{/* System Theme Button */}
|
||||||
|
|
@ -90,7 +90,11 @@ export const ThemeButtons = () => {
|
||||||
data-testid="menu_system_button"
|
data-testid="menu_system_button"
|
||||||
id="menu_system_button"
|
id="menu_system_button"
|
||||||
>
|
>
|
||||||
<ForwardedIconComponent name="Monitor" className="w-4" />
|
<ForwardedIconComponent
|
||||||
|
name="Monitor"
|
||||||
|
className="w-4"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
|
import { useDarkStore } from "@/stores/darkStore";
|
||||||
|
import { formatNumber } from "@/utils/utils";
|
||||||
|
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||||
|
|
||||||
|
export const LangflowCounts = () => {
|
||||||
|
const stars: number | undefined = useDarkStore((state) => state.stars);
|
||||||
|
const discordCount: number = useDarkStore((state) => state.discordCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<ShadTooltip
|
||||||
|
content="Go to GitHub repo"
|
||||||
|
side="bottom"
|
||||||
|
styleClasses="z-10"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground">
|
||||||
|
<FaGithub className="h-4 w-4" />
|
||||||
|
<span className="text-xs font-semibold">{formatNumber(stars)}</span>
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
|
||||||
|
<ShadTooltip
|
||||||
|
content="Go to Discord server"
|
||||||
|
side="bottom"
|
||||||
|
styleClasses="z-10"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground">
|
||||||
|
<FaDiscord className="h-4 w-4" />
|
||||||
|
<span className="text-xs font-semibold">
|
||||||
|
{formatNumber(discordCount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LangflowCounts;
|
||||||
|
|
@ -12,10 +12,12 @@ import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||||
import useTheme from "@/customization/hooks/use-custom-theme";
|
import useTheme from "@/customization/hooks/use-custom-theme";
|
||||||
import { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all";
|
import { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all";
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
|
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
|
import { useFolderStore } from "@/stores/foldersStore";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { AccountMenu } from "./components/AccountMenu";
|
import { AccountMenu } from "./components/AccountMenu";
|
||||||
import FlowMenu from "./components/FlowMenu";
|
import FlowMenu from "./components/FlowMenu";
|
||||||
import GithubStarComponent from "./components/GithubStarButton";
|
import LangflowCounts from "./components/langflow-counts";
|
||||||
|
|
||||||
export default function AppHeader(): JSX.Element {
|
export default function AppHeader(): JSX.Element {
|
||||||
const notificationCenter = useAlertStore((state) => state.notificationCenter);
|
const notificationCenter = useAlertStore((state) => state.notificationCenter);
|
||||||
|
|
@ -46,9 +48,17 @@ export default function AppHeader(): JSX.Element {
|
||||||
|
|
||||||
useResetDismissUpdateAll();
|
useResetDismissUpdateAll();
|
||||||
|
|
||||||
|
const flows = useFlowsManagerStore((state) => state.flows);
|
||||||
|
const examples = useFlowsManagerStore((state) => state.examples);
|
||||||
|
const folders = useFolderStore((state) => state.folders);
|
||||||
|
|
||||||
|
const isEmpty = flows?.length !== examples?.length || folders?.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex h-[62px] w-full items-center justify-between gap-2 border-b px-5 py-2.5 dark:bg-background"
|
className={`flex h-[44px] w-full items-center justify-between border-b p-6 dark:bg-background ${
|
||||||
|
!isEmpty ? "hidden" : ""
|
||||||
|
}`}
|
||||||
data-testid="app-header"
|
data-testid="app-header"
|
||||||
>
|
>
|
||||||
{/* Left Section */}
|
{/* Left Section */}
|
||||||
|
|
@ -83,7 +93,7 @@ export default function AppHeader(): JSX.Element {
|
||||||
|
|
||||||
{/* Right Section */}
|
{/* Right Section */}
|
||||||
<div
|
<div
|
||||||
className={`z-30 flex items-center gap-2`}
|
className={`relative left-3 z-30 flex items-center gap-2`}
|
||||||
data-testid="header_right_section_wrapper"
|
data-testid="header_right_section_wrapper"
|
||||||
>
|
>
|
||||||
{!ENABLE_DATASTAX_LANGFLOW && (
|
{!ENABLE_DATASTAX_LANGFLOW && (
|
||||||
|
|
@ -95,7 +105,7 @@ export default function AppHeader(): JSX.Element {
|
||||||
window.open("https://github.com/langflow-ai/langflow", "_blank")
|
window.open("https://github.com/langflow-ai/langflow", "_blank")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GithubStarComponent />
|
<LangflowCounts />
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -111,8 +121,7 @@ export default function AppHeader(): JSX.Element {
|
||||||
<AlertDropdown onClose={() => setActiveState(null)}>
|
<AlertDropdown onClose={() => setActiveState(null)}>
|
||||||
<Button
|
<Button
|
||||||
ref={notificationRef}
|
ref={notificationRef}
|
||||||
variant="ghost"
|
unstyled
|
||||||
className={`relative ${activeState === "notifications" ? "bg-accent text-accent-foreground" : ""}`}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setActiveState((prev) =>
|
setActiveState((prev) =>
|
||||||
prev === "notifications" ? null : "notifications",
|
prev === "notifications" ? null : "notifications",
|
||||||
|
|
@ -123,47 +132,24 @@ export default function AppHeader(): JSX.Element {
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
notificationCenter
|
notificationCenter
|
||||||
? `absolute left-[31px] top-[10px] h-1 w-1 rounded-full bg-destructive`
|
? `absolute right-[5.3rem] top-[10px] h-1 w-1 rounded-full bg-destructive`
|
||||||
: "hidden"
|
: "hidden"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name="Bell"
|
name="Bell"
|
||||||
className="side-bar-button-size h-[18px] w-[18px]"
|
className="side-bar-button-size ml-1 h-4 w-4 text-muted-foreground"
|
||||||
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
<span className="hidden whitespace-nowrap">Notifications</span>
|
<span className="hidden whitespace-nowrap">Notifications</span>
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDropdown>
|
</AlertDropdown>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
</AlertDropdown>
|
</AlertDropdown>
|
||||||
{!ENABLE_DATASTAX_LANGFLOW && (
|
<Separator
|
||||||
<>
|
orientation="vertical"
|
||||||
<ShadTooltip
|
className="my-auto ml-3 h-7 dark:border-zinc-700"
|
||||||
content="Go to Langflow Store"
|
/>
|
||||||
side="bottom"
|
|
||||||
styleClasses="z-10"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className={` ${lastPath === "store" ? "bg-accent text-accent-foreground" : ""} z-50`}
|
|
||||||
onClick={() => {
|
|
||||||
navigate("/store");
|
|
||||||
}}
|
|
||||||
data-testid="button-store"
|
|
||||||
>
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="Store"
|
|
||||||
className="side-bar-button-size h-[18px] w-[18px]"
|
|
||||||
/>
|
|
||||||
<span className="hidden whitespace-nowrap">Store</span>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="my-auto h-7 dark:border-zinc-700"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{ENABLE_DATASTAX_LANGFLOW && (
|
{ENABLE_DATASTAX_LANGFLOW && (
|
||||||
<>
|
<>
|
||||||
<ShadTooltip content="Docs" side="bottom" styleClasses="z-10">
|
<ShadTooltip content="Docs" side="bottom" styleClasses="z-10">
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ export default function CardsWrapComponent({
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-full",
|
"h-full w-full",
|
||||||
isDragging
|
isDragging
|
||||||
? "mb-36 flex flex-col items-center justify-center gap-4 text-2xl font-light"
|
? "z-10 mb-36 flex flex-col items-center justify-center gap-4 text-2xl font-light"
|
||||||
: "",
|
: "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
import IconComponent from "@/components/common/genericIconComponent";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DISCORD_URL, GITHUB_URL } from "@/constants/constants";
|
||||||
|
import { useGetUserData, useUpdateUser } from "@/controllers/API/queries/auth";
|
||||||
|
import ModalsComponent from "@/pages/MainPage/components/modalsComponent";
|
||||||
|
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
|
import { Users } from "@/types/api";
|
||||||
|
import { cn } from "@/utils/utils";
|
||||||
|
import { FC, useEffect, useMemo, useState } from "react";
|
||||||
|
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||||
|
|
||||||
|
export const GetStartedProgress: FC<{
|
||||||
|
userData: Users;
|
||||||
|
isGithubStarred: boolean;
|
||||||
|
isDiscordJoined: boolean;
|
||||||
|
handleDismissDialog: () => void;
|
||||||
|
}> = ({ userData, isGithubStarred, isDiscordJoined, handleDismissDialog }) => {
|
||||||
|
const [isGithubStarredChild, setIsGithubStarredChild] =
|
||||||
|
useState(isGithubStarred);
|
||||||
|
const [isDiscordJoinedChild, setIsDiscordJoinedChild] =
|
||||||
|
useState(isDiscordJoined);
|
||||||
|
const [newProjectModal, setNewProjectModal] = useState(false);
|
||||||
|
|
||||||
|
const flows = useFlowsManagerStore((state) => state.flows);
|
||||||
|
|
||||||
|
const { mutate: mutateLoggedUser } = useGetUserData();
|
||||||
|
const { mutate: updateUser } = useUpdateUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userData) {
|
||||||
|
mutateLoggedUser(null);
|
||||||
|
}
|
||||||
|
}, [userData, mutateLoggedUser]);
|
||||||
|
|
||||||
|
const hasFlows = flows && flows?.length > 0;
|
||||||
|
|
||||||
|
const percentageGetStarted = useMemo(() => {
|
||||||
|
const totalSteps = 3;
|
||||||
|
let hasFlowsCount = 0;
|
||||||
|
const completedSteps = Object.keys(userData?.optins ?? {}).filter(
|
||||||
|
(key) => userData?.optins?.[key],
|
||||||
|
)?.length;
|
||||||
|
|
||||||
|
if (hasFlows) {
|
||||||
|
hasFlowsCount = 33;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentage =
|
||||||
|
Math.round((completedSteps / totalSteps) * 100) + hasFlowsCount;
|
||||||
|
|
||||||
|
if (percentage > 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return percentage;
|
||||||
|
}, [userData?.optins, isGithubStarredChild, isDiscordJoinedChild, hasFlows]);
|
||||||
|
|
||||||
|
const handleUserTrack = (key: string) => {
|
||||||
|
const optins = userData?.optins ?? {};
|
||||||
|
optins[key] = true;
|
||||||
|
|
||||||
|
updateUser(
|
||||||
|
{
|
||||||
|
user_id: userData?.id!,
|
||||||
|
user: { optins },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
mutateLoggedUser({});
|
||||||
|
if (key === "github_starred") {
|
||||||
|
setIsGithubStarredChild(true);
|
||||||
|
window.open(GITHUB_URL, "_blank", "noopener,noreferrer");
|
||||||
|
} else if (key === "discord_clicked") {
|
||||||
|
setIsDiscordJoinedChild(true);
|
||||||
|
window.open(DISCORD_URL, "_blank", "noopener,noreferrer");
|
||||||
|
} else if (key === "dialog_dismissed") {
|
||||||
|
handleDismissDialog();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[180px] w-full">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<span className="font-[14px]">Get started</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleUserTrack("dialog_dismissed")}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
data-testid="close_get_started_dialog"
|
||||||
|
>
|
||||||
|
<IconComponent name="X" className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1 flex items-center justify-between gap-3">
|
||||||
|
<div className="h-1 w-full rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
className="h-1 w-[33%] rounded-full bg-accent-pink-foreground"
|
||||||
|
style={{ width: `${percentageGetStarted}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="text-xs text-muted-foreground"
|
||||||
|
data-testid="get_started_progress_percentage"
|
||||||
|
>
|
||||||
|
{percentageGetStarted}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Button
|
||||||
|
data-testid="github_starred_btn_get_started"
|
||||||
|
unstyled
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
isGithubStarredChild && "pointer-events-none",
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (isGithubStarredChild) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleUserTrack("github_starred");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md p-2 hover:bg-muted",
|
||||||
|
isGithubStarredChild && "pointer-events-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isGithubStarredChild ? (
|
||||||
|
<span data-testid="github_starred_icon_get_started">
|
||||||
|
<IconComponent
|
||||||
|
name="Check"
|
||||||
|
className="h-4 w-4 text-accent-emerald-foreground"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<FaGithub className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
isGithubStarredChild && "text-muted-foreground line-through",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Star repo for updates
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
data-testid="discord_joined_btn_get_started"
|
||||||
|
unstyled
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
isDiscordJoinedChild && "pointer-events-none",
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (isDiscordJoinedChild) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleUserTrack("discord_clicked");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md p-2 hover:bg-muted",
|
||||||
|
isDiscordJoinedChild && "pointer-events-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isDiscordJoinedChild ? (
|
||||||
|
<span data-testid="discord_joined_icon_get_started">
|
||||||
|
<IconComponent
|
||||||
|
name="Check"
|
||||||
|
className="h-4 w-4 text-accent-emerald-foreground"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<FaDiscord className="h-4 w-4 text-[#5865F2]" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
isDiscordJoinedChild && "text-muted-foreground line-through",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Join the community
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
className={cn("w-full", hasFlows && "pointer-events-none")}
|
||||||
|
onClick={() => setNewProjectModal(true)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md p-2 hover:bg-muted",
|
||||||
|
hasFlows && "pointer-events-none text-muted-foreground",
|
||||||
|
)}
|
||||||
|
data-testid="create_flow_btn_get_started"
|
||||||
|
>
|
||||||
|
<span data-testid="create_flow_icon_get_started">
|
||||||
|
<IconComponent
|
||||||
|
name={hasFlows ? "Check" : "Plus"}
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 text-primary",
|
||||||
|
hasFlows && "text-accent-emerald-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className={cn("text-sm", hasFlows && "line-through")}>
|
||||||
|
Create a flow
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalsComponent
|
||||||
|
openModal={newProjectModal}
|
||||||
|
setOpenModal={setNewProjectModal}
|
||||||
|
openDeleteFolderModal={false}
|
||||||
|
setOpenDeleteFolderModal={() => {}}
|
||||||
|
handleDeleteFolder={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import IconComponent from "@/components/common/genericIconComponent";
|
import IconComponent from "@/components/common/genericIconComponent";
|
||||||
|
import { GetStartedProgress } from "@/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/get-started-progress";
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
import useAuthStore from "@/stores/authStore";
|
||||||
|
import { Separator } from "@radix-ui/react-separator";
|
||||||
|
import { useState } from "react";
|
||||||
import { AddFolderButton } from "./add-folder-button";
|
import { AddFolderButton } from "./add-folder-button";
|
||||||
import { UploadFolderButton } from "./upload-folder-button";
|
import { UploadFolderButton } from "./upload-folder-button";
|
||||||
|
|
||||||
|
|
@ -13,23 +17,54 @@ export const HeaderButtons = ({
|
||||||
isUpdatingFolder: boolean;
|
isUpdatingFolder: boolean;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
addNewFolder: () => void;
|
addNewFolder: () => void;
|
||||||
}) => (
|
}) => {
|
||||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
const userData = useAuthStore((state) => state.userData);
|
||||||
<SidebarTrigger className="lg:hidden">
|
const userDismissedDialog = userData?.optins?.dialog_dismissed;
|
||||||
<IconComponent name="PanelLeftClose" className="h-4 w-4" />
|
const isGithubStarred = userData?.optins?.github_starred;
|
||||||
</SidebarTrigger>
|
const isDiscordJoined = userData?.optins?.discord_clicked;
|
||||||
|
|
||||||
<div className="flex-1 text-sm font-semibold">Folders</div>
|
const [isDismissedDialog, setIsDismissedDialog] =
|
||||||
<div className="flex items-center gap-1">
|
useState(userDismissedDialog);
|
||||||
<UploadFolderButton
|
|
||||||
onClick={handleUploadFlowsToFolder}
|
const handleDismissDialog = () => {
|
||||||
disabled={isUpdatingFolder}
|
setIsDismissedDialog(true);
|
||||||
/>
|
};
|
||||||
<AddFolderButton
|
|
||||||
onClick={addNewFolder}
|
return (
|
||||||
disabled={isUpdatingFolder}
|
<>
|
||||||
loading={isPending}
|
{!isDismissedDialog && (
|
||||||
/>
|
<>
|
||||||
</div>
|
<GetStartedProgress
|
||||||
</div>
|
userData={userData!}
|
||||||
);
|
isGithubStarred={isGithubStarred ?? false}
|
||||||
|
isDiscordJoined={isDiscordJoined ?? false}
|
||||||
|
handleDismissDialog={handleDismissDialog}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="-mx-4 mt-4 w-[280px]">
|
||||||
|
<hr className="border-t-1 w-full" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex shrink-0 items-center justify-between gap-2 pt-2">
|
||||||
|
<SidebarTrigger className="lg:hidden">
|
||||||
|
<IconComponent name="PanelLeftClose" className="h-4 w-4" />
|
||||||
|
</SidebarTrigger>
|
||||||
|
|
||||||
|
<div className="flex-1 text-sm font-semibold">Folders</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<UploadFolderButton
|
||||||
|
onClick={handleUploadFlowsToFolder}
|
||||||
|
disabled={isUpdatingFolder}
|
||||||
|
/>
|
||||||
|
<AddFolderButton
|
||||||
|
onClick={addNewFolder}
|
||||||
|
disabled={isUpdatingFolder}
|
||||||
|
loading={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
99
src/frontend/src/components/ui/background-gradient.tsx
Normal file
99
src/frontend/src/components/ui/background-gradient.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { cn } from "@/utils/utils";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const BackgroundGradient = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
animate = true,
|
||||||
|
borderColor,
|
||||||
|
borderRadius,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
animate?: boolean;
|
||||||
|
borderColor?: string;
|
||||||
|
borderRadius?: string;
|
||||||
|
}) => {
|
||||||
|
const variants = {
|
||||||
|
initial: {
|
||||||
|
backgroundPosition: "0 50%",
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
backgroundPosition: ["0, 50%", "100% 50%", "0 50%"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultGradient =
|
||||||
|
"radial-gradient(circle farthest-side at 0 100%,#00ccb1,transparent),radial-gradient(circle farthest-side at 100% 0,#7b61ff,transparent),radial-gradient(circle farthest-side at 100% 100%,#ffc414,transparent),radial-gradient(circle farthest-side at 0 0,#1ca0fb,#141316)";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("group relative p-[1px]", containerClassName)}>
|
||||||
|
<motion.div
|
||||||
|
variants={animate ? variants : undefined}
|
||||||
|
initial={animate ? "initial" : undefined}
|
||||||
|
animate={animate ? "animate" : undefined}
|
||||||
|
transition={
|
||||||
|
animate
|
||||||
|
? {
|
||||||
|
duration: 5,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "reverse",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
backgroundSize: animate ? "400% 400%" : undefined,
|
||||||
|
background: borderColor || defaultGradient,
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
borderRadius: "24px",
|
||||||
|
opacity: 0.2,
|
||||||
|
filter: "blur(8px)",
|
||||||
|
transition: "all 0.4s ease-in-out",
|
||||||
|
willChange: "transform",
|
||||||
|
}}
|
||||||
|
className="group-hover:filter-[blur(15px)] group-hover:opacity-70 group-hover:brightness-125"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
variants={animate ? variants : undefined}
|
||||||
|
initial={animate ? "initial" : undefined}
|
||||||
|
animate={animate ? "animate" : undefined}
|
||||||
|
transition={
|
||||||
|
animate
|
||||||
|
? {
|
||||||
|
duration: 5,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "reverse",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
backgroundSize: animate ? "400% 400%" : undefined,
|
||||||
|
background: borderColor || defaultGradient,
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
borderRadius: borderRadius || "24px",
|
||||||
|
willChange: "transform",
|
||||||
|
transition: "all 0.4s ease-in-out",
|
||||||
|
"--border-color": borderColor,
|
||||||
|
"--border-color-transparent": borderColor
|
||||||
|
? `${borderColor}20`
|
||||||
|
: undefined,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"group-hover:brightness-125",
|
||||||
|
borderColor
|
||||||
|
? "group-hover:shadow-[0_0_15px_3px_var(--border-color),0_0_25px_5px_var(--border-color-transparent)]"
|
||||||
|
: "group-hover:shadow-[0_0_15px_3px_rgba(0,204,177,0.3),0_0_25px_5px_rgba(123,97,255,0.2)]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={cn("relative z-10", className)}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
src/frontend/src/components/ui/dot-background.tsx
Normal file
36
src/frontend/src/components/ui/dot-background.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { cn } from "@/utils/utils";
|
||||||
|
|
||||||
|
export function DotBackgroundDemo({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-full w-full items-center justify-center bg-white dark:bg-black",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0",
|
||||||
|
"[background-size:20px_20px]",
|
||||||
|
"[background-image:radial-gradient(#d4d4d4_1px,transparent_1px)]",
|
||||||
|
"dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 bg-gradient-to-b from-white/10 via-white/50 to-white dark:from-background/30 dark:via-background/80 dark:to-background",
|
||||||
|
containerClassName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1005,6 +1005,7 @@ export const GRADIENT_CLASS_DISABLED =
|
||||||
"linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))";
|
"linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))";
|
||||||
|
|
||||||
export const RECEIVING_INPUT_VALUE = "Receiving input";
|
export const RECEIVING_INPUT_VALUE = "Receiving input";
|
||||||
|
export const SELECT_AN_OPTION = "Select an option...";
|
||||||
|
|
||||||
export const ICON_STROKE_WIDTH = 1.5;
|
export const ICON_STROKE_WIDTH = 1.5;
|
||||||
|
|
||||||
|
|
@ -1068,5 +1069,9 @@ export const OPENAI_VOICES = [
|
||||||
export const DEFAULT_POLLING_INTERVAL = 5000;
|
export const DEFAULT_POLLING_INTERVAL = 5000;
|
||||||
export const DEFAULT_TIMEOUT = 30000;
|
export const DEFAULT_TIMEOUT = 30000;
|
||||||
export const DEFAULT_FILE_PICKER_TIMEOUT = 60000;
|
export const DEFAULT_FILE_PICKER_TIMEOUT = 60000;
|
||||||
|
export const DISCORD_URL = "https://discord.com/invite/EqksyE2EX9";
|
||||||
|
export const GITHUB_URL = "https://github.com/langflow-ai/langflow";
|
||||||
|
export const TWITTER_URL = "https://x.com/langflow_ai";
|
||||||
|
export const DOCS_URL = "https://docs.langflow.org";
|
||||||
|
|
||||||
export const UUID_PARSING_ERROR = "uuid_parsing";
|
export const UUID_PARSING_ERROR = "uuid_parsing";
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import { FlowStyleType, FlowType } from "../../types/flow";
|
||||||
import { StoreComponentResponse } from "../../types/store";
|
import { StoreComponentResponse } from "../../types/store";
|
||||||
|
|
||||||
const GITHUB_API_URL = "https://api.github.com";
|
const GITHUB_API_URL = "https://api.github.com";
|
||||||
|
const DISCORD_API_URL =
|
||||||
|
"https://discord.com/api/v9/invites/EqksyE2EX9?with_counts=true";
|
||||||
|
|
||||||
export async function getRepoStars(owner: string, repo: string) {
|
export async function getRepoStars(owner: string, repo: string) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -21,6 +23,15 @@ export async function getRepoStars(owner: string, repo: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDiscordCount() {
|
||||||
|
try {
|
||||||
|
const response = await api.get(DISCORD_API_URL);
|
||||||
|
return response?.data.approximate_member_count;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching repository data:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function createApiKey(name: string) {
|
export async function createApiKey(name: string) {
|
||||||
try {
|
try {
|
||||||
const res = await api.post(`${BASE_URL_API}api_key/`, { name });
|
const res = await api.post(`${BASE_URL_API}api_key/`, { name });
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
|
import useAuthStore from "@/stores/authStore";
|
||||||
import { UseMutationResult } from "@tanstack/react-query";
|
import { UseMutationResult } from "@tanstack/react-query";
|
||||||
import { useMutationFunctionType, Users } from "../../../../types/api";
|
import { useMutationFunctionType, Users } from "../../../../types/api";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { getURL } from "../../helpers/constants";
|
import { getURL } from "../../helpers/constants";
|
||||||
import { UseRequestProcessor } from "../../services/request-processor";
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
||||||
export const useGetUserData: useMutationFunctionType<undefined, any> = (
|
export const useGetUserData: useMutationFunctionType<undefined, any> = (
|
||||||
options?,
|
options?,
|
||||||
) => {
|
) => {
|
||||||
|
const setUserData = useAuthStore((state) => state.setUserData);
|
||||||
const { mutate } = UseRequestProcessor();
|
const { mutate } = UseRequestProcessor();
|
||||||
|
|
||||||
const getUserData = async () => {
|
const getUserData = async () => {
|
||||||
const response = await api.get<Users>(`${getURL("USERS")}/whoami`);
|
const response = await api.get<Users>(`${getURL("USERS")}/whoami`);
|
||||||
|
setUserData(response["data"]);
|
||||||
return response["data"];
|
return response["data"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
interface versionQueryResponse {
|
interface versionQueryResponse {
|
||||||
version: string;
|
version: string;
|
||||||
package: string;
|
package: string;
|
||||||
|
main_version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGetVersionQuery: useQueryFunctionType<
|
export const useGetVersionQuery: useQueryFunctionType<
|
||||||
|
|
@ -22,7 +23,9 @@ export const useGetVersionQuery: useQueryFunctionType<
|
||||||
const responseFn = async () => {
|
const responseFn = async () => {
|
||||||
const { data } = await getVersionFn();
|
const { data } = await getVersionFn();
|
||||||
const refreshVersion = useDarkStore.getState().refreshVersion;
|
const refreshVersion = useDarkStore.getState().refreshVersion;
|
||||||
|
const refreshLatestVersion = useDarkStore.getState().refreshLatestVersion;
|
||||||
refreshVersion(data.version);
|
refreshVersion(data.version);
|
||||||
|
refreshLatestVersion(data.main_version);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
46
src/frontend/src/icons/Twitter X/TwitterX.jsx
Normal file
46
src/frontend/src/icons/Twitter X/TwitterX.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
const TwitterXSVG = (props) => {
|
||||||
|
return props.isdark === "true" ? (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0,0,256,256"
|
||||||
|
width="30px"
|
||||||
|
height="30px"
|
||||||
|
fillRule="nonzero"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="#ffffff"
|
||||||
|
fillRule="nonzero"
|
||||||
|
stroke="none"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
strokeDasharray=""
|
||||||
|
strokeDashoffset="0"
|
||||||
|
fontFamily="none"
|
||||||
|
fontWeight="none"
|
||||||
|
fontSize="none"
|
||||||
|
textAnchor="none"
|
||||||
|
style={{ mixBlendMode: "normal" }}
|
||||||
|
>
|
||||||
|
<g transform="scale(8.53333,8.53333)">
|
||||||
|
<path d="M26.37,26l-8.795,-12.822l0.015,0.012l7.93,-9.19h-2.65l-6.46,7.48l-5.13,-7.48h-6.95l8.211,11.971l-0.001,-0.001l-8.66,10.03h2.65l7.182,-8.322l5.708,8.322zM10.23,6l12.34,18h-2.1l-12.35,-18z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 30 30"
|
||||||
|
width="30px"
|
||||||
|
height="30px"
|
||||||
|
fill="#000000"
|
||||||
|
>
|
||||||
|
<path d="M26.37,26l-8.795-12.822l0.015,0.012L25.52,4h-2.65l-6.46,7.48L11.28,4H4.33l8.211,11.971L12.54,15.97L3.88,26h2.65 l7.182-8.322L19.42,26H26.37z M10.23,6l12.34,18h-2.1L8.12,6H10.23z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TwitterXSVG;
|
||||||
1
src/frontend/src/icons/Twitter X/iconX.svg
Normal file
1
src/frontend/src/icons/Twitter X/iconX.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="30px" height="30px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(8.53333,8.53333)"><path d="M26.37,26l-8.795,-12.822l0.015,0.012l7.93,-9.19h-2.65l-6.46,7.48l-5.13,-7.48h-6.95l8.211,11.971l-0.001,-0.001l-8.66,10.03h2.65l7.182,-8.322l5.708,8.322zM10.23,6l12.34,18h-2.1l-12.35,-18z"></path></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 693 B |
1
src/frontend/src/icons/Twitter X/icons8-x.svg
Normal file
1
src/frontend/src/icons/Twitter X/icons8-x.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="30px" height="30px"><path d="M26.37,26l-8.795-12.822l0.015,0.012L25.52,4h-2.65l-6.46,7.48L11.28,4H4.33l8.211,11.971L12.54,15.97L3.88,26h2.65 l7.182-8.322L19.42,26H26.37z M10.23,6l12.34,18h-2.1L8.12,6H10.23z"/></svg>
|
||||||
|
After Width: | Height: | Size: 283 B |
11
src/frontend/src/icons/Twitter X/index.tsx
Normal file
11
src/frontend/src/icons/Twitter X/index.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { useDarkStore } from "@/stores/darkStore";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
import TwitterXSVG from "./TwitterX.jsx";
|
||||||
|
|
||||||
|
export const TwitterXIcon = forwardRef<
|
||||||
|
SVGSVGElement,
|
||||||
|
React.PropsWithChildren<{}>
|
||||||
|
>((props, ref) => {
|
||||||
|
const isdark = useDarkStore((state) => state.dark).toString();
|
||||||
|
return <TwitterXSVG ref={ref} isdark={isdark} {...props} />;
|
||||||
|
});
|
||||||
|
|
@ -231,6 +231,7 @@ import {
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { TwitterXIcon } from "./Twitter X";
|
||||||
|
|
||||||
// Create a map of eagerly loaded Lucide icons
|
// Create a map of eagerly loaded Lucide icons
|
||||||
export const lucideIcons = {
|
export const lucideIcons = {
|
||||||
|
|
@ -464,4 +465,5 @@ export const lucideIcons = {
|
||||||
Zap,
|
Zap,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
|
TwitterXIcon,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export default function DeleteConfirmationModal({
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="mr-1"
|
className="mr-1"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
data-testid="btn_cancel_delete_confirmation_modal"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -72,6 +73,7 @@ export default function DeleteConfirmationModal({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
onConfirm(e);
|
onConfirm(e);
|
||||||
}}
|
}}
|
||||||
|
data-testid="btn_delete_delete_confirmation_modal"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ import { LoadingPage } from "../LoadingPage";
|
||||||
|
|
||||||
export function AppInitPage() {
|
export function AppInitPage() {
|
||||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||||
|
const refreshDiscordCount = useDarkStore(
|
||||||
|
(state) => state.refreshDiscordCount,
|
||||||
|
);
|
||||||
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
||||||
|
|
||||||
const { isFetched: isLoaded } = useCustomPrimaryLoading();
|
const { isFetched: isLoaded } = useCustomPrimaryLoading();
|
||||||
|
|
@ -31,6 +34,7 @@ export function AppInitPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFetched) {
|
if (isFetched) {
|
||||||
refreshStars();
|
refreshStars();
|
||||||
|
refreshDiscordCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isConfigFetched) {
|
if (isConfigFetched) {
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ const DropdownComponent = ({
|
||||||
setOpenDelete(true);
|
setOpenDelete(true);
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer text-destructive"
|
className="cursor-pointer text-destructive"
|
||||||
|
data-testid="btn_delete_dropdown_menu"
|
||||||
>
|
>
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name="Trash2"
|
name="Trash2"
|
||||||
|
|
|
||||||
190
src/frontend/src/pages/MainPage/pages/empty-page.tsx
Normal file
190
src/frontend/src/pages/MainPage/pages/empty-page.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import LangflowLogo from "@/assets/LangflowLogo.svg?react";
|
||||||
|
import logoDarkPng from "@/assets/logo_dark.png";
|
||||||
|
import logoLightPng from "@/assets/logo_light.png";
|
||||||
|
import CardsWrapComponent from "@/components/core/cardsWrapComponent";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DotBackgroundDemo } from "@/components/ui/dot-background";
|
||||||
|
import { DISCORD_URL, GITHUB_URL } from "@/constants/constants";
|
||||||
|
import { useGetUserData, useUpdateUser } from "@/controllers/API/queries/auth";
|
||||||
|
import { EnhancedBeamEffect } from "@/pages/MainPage/pages/enchanced-beam-effect";
|
||||||
|
import useAuthStore from "@/stores/authStore";
|
||||||
|
import { useDarkStore } from "@/stores/darkStore";
|
||||||
|
import { useFolderStore } from "@/stores/foldersStore";
|
||||||
|
import { formatNumber } from "@/utils/utils";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||||
|
import { HiArrowRight } from "react-icons/hi";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import useFileDrop from "../hooks/use-on-file-drop";
|
||||||
|
|
||||||
|
const EMPTY_PAGE_TITLE = "Your new favorite way to ship Agents";
|
||||||
|
const EMPTY_PAGE_DESCRIPTION =
|
||||||
|
"Design agents that connect to any API, model, or database.";
|
||||||
|
const EMPTY_PAGE_GITHUB_DESCRIPTION =
|
||||||
|
"Follow development, star the repo, and shape the future.";
|
||||||
|
const EMPTY_PAGE_DISCORD_DESCRIPTION =
|
||||||
|
"Join builders, ask questions, and show off your agents.";
|
||||||
|
const EMPTY_PAGE_DRAG_AND_DROP_TEXT =
|
||||||
|
"Already have a flow? Drag and drop to upload.";
|
||||||
|
const EMPTY_PAGE_FOLDER_DESCRIPTION = "Empty folder";
|
||||||
|
const EMPTY_PAGE_CREATE_FIRST_FLOW_BUTTON_TEXT = <>+ Create first flow</>;
|
||||||
|
|
||||||
|
const EXTERNAL_LINK_ICON_CLASS =
|
||||||
|
"absolute right-6 top-[35px] h-4 w-4 shrink-0 translate-x-0 opacity-0 transition-all duration-300 group-hover:translate-x-1 group-hover:opacity-100";
|
||||||
|
|
||||||
|
export const EmptyPageCommunity = ({
|
||||||
|
setOpenModal,
|
||||||
|
}: {
|
||||||
|
setOpenModal: (open: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const handleFileDrop = useFileDrop(undefined);
|
||||||
|
const folders = useFolderStore((state) => state.folders);
|
||||||
|
const userData = useAuthStore(useShallow((state) => state.userData));
|
||||||
|
const stars: number | undefined = useDarkStore((state) => state.stars);
|
||||||
|
const discordCount: number = useDarkStore((state) => state.discordCount);
|
||||||
|
const { mutate: updateUser } = useUpdateUser();
|
||||||
|
const { mutate: mutateLoggedUser } = useGetUserData();
|
||||||
|
|
||||||
|
const handleUserTrack = (key: string) => () => {
|
||||||
|
const optins = userData?.optins ?? {};
|
||||||
|
optins[key] = true;
|
||||||
|
updateUser(
|
||||||
|
{
|
||||||
|
user_id: userData?.id!,
|
||||||
|
user: { optins },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
mutateLoggedUser({});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DotBackgroundDemo>
|
||||||
|
<CardsWrapComponent
|
||||||
|
dragMessage={`Drop your flows or components here`}
|
||||||
|
onFileDrop={handleFileDrop}
|
||||||
|
>
|
||||||
|
<div className="m-0 h-full w-full bg-background p-0">
|
||||||
|
<div className="z-50 flex h-full w-full flex-col items-center justify-center gap-6">
|
||||||
|
<div className="z-50 flex flex-col items-center gap-3">
|
||||||
|
<div className="z-50 dark:hidden">
|
||||||
|
<img
|
||||||
|
src={logoLightPng}
|
||||||
|
alt="Langflow Logo Light"
|
||||||
|
data-testid="empty_page_logo_light"
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="z-50 hidden dark:block">
|
||||||
|
<img
|
||||||
|
src={logoDarkPng}
|
||||||
|
alt="Langflow Logo Dark"
|
||||||
|
data-testid="empty_page_logo_dark"
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
data-testid="mainpage_title"
|
||||||
|
className="z-50 text-center text-2xl font-semibold text-foreground"
|
||||||
|
>
|
||||||
|
{EMPTY_PAGE_TITLE}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
data-testid="empty_page_description"
|
||||||
|
className="z-50 text-center text-[14px] text-secondary-foreground"
|
||||||
|
>
|
||||||
|
{folders?.length > 1
|
||||||
|
? EMPTY_PAGE_FOLDER_DESCRIPTION
|
||||||
|
: EMPTY_PAGE_DESCRIPTION}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full max-w-[510px] flex-col gap-12 sm:gap-8">
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
className="group mx-3 h-[84px] sm:mx-0"
|
||||||
|
onClick={() => {
|
||||||
|
handleUserTrack("github_starred")();
|
||||||
|
window.open(GITHUB_URL, "_blank", "noopener,noreferrer");
|
||||||
|
}}
|
||||||
|
data-testid="empty_page_github_button"
|
||||||
|
>
|
||||||
|
<div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-accent-pink-foreground hover:shadow-[0_4px_10px_rgba(198,97,184,0.25)]">
|
||||||
|
<div className="grid w-full items-center justify-between gap-2">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<FaGithub className="h-6 w-6" />
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold">GitHub</span>
|
||||||
|
<span className="ml-2 font-mono text-muted-foreground">
|
||||||
|
{formatNumber(stars)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-secondary-foreground">
|
||||||
|
{EMPTY_PAGE_GITHUB_DESCRIPTION}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ExternalLink className={EXTERNAL_LINK_ICON_CLASS} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
className="group mx-3 h-[84px] sm:mx-0"
|
||||||
|
onClick={() => {
|
||||||
|
handleUserTrack("discord_joined")();
|
||||||
|
window.open(DISCORD_URL, "_blank", "noopener,noreferrer");
|
||||||
|
}}
|
||||||
|
data-testid="empty_page_discord_button"
|
||||||
|
>
|
||||||
|
<div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-discord-color hover:shadow-[0_4px_10px_rgba(88,101,242,0.25)]">
|
||||||
|
<div className="grid w-full items-center justify-between gap-2">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<FaDiscord className="h-6 w-6 text-discord-color" />
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold">Discord</span>
|
||||||
|
<span className="ml-2 font-mono text-muted-foreground">
|
||||||
|
{formatNumber(discordCount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-secondary-foreground">
|
||||||
|
{EMPTY_PAGE_DISCORD_DESCRIPTION}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ExternalLink className={EXTERNAL_LINK_ICON_CLASS} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="z-10 m-auto h-10 w-full max-w-[155px] rounded-lg font-bold transition-all duration-300"
|
||||||
|
onClick={() => setOpenModal(true)}
|
||||||
|
id="new-project-btn"
|
||||||
|
data-testid="new_project_btn_empty_page"
|
||||||
|
>
|
||||||
|
<span>{EMPTY_PAGE_CREATE_FIRST_FLOW_BUTTON_TEXT}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
data-testid="empty_page_drag_and_drop_text"
|
||||||
|
className="absolute bottom-5 left-0 right-0 mt-4 cursor-default text-center text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
{EMPTY_PAGE_DRAG_AND_DROP_TEXT}
|
||||||
|
</p>
|
||||||
|
</CardsWrapComponent>
|
||||||
|
</DotBackgroundDemo>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyPageCommunity;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { cn } from "@/utils/utils";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { BorderBeam } from "../../../components/ui/border-beams";
|
||||||
|
|
||||||
|
interface EnhancedBeamEffectProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
secondaryColor?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnhancedBeamEffect = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
primaryColor = "#C661B8",
|
||||||
|
secondaryColor = "#61C6B8",
|
||||||
|
size = 200,
|
||||||
|
}: EnhancedBeamEffectProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex items-center justify-center overflow-hidden rounded-xl",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Primary beam - larger, slower rotation */}
|
||||||
|
<BorderBeam
|
||||||
|
duration={12}
|
||||||
|
size={size}
|
||||||
|
className="opacity-80"
|
||||||
|
colorFrom={primaryColor}
|
||||||
|
colorTo={secondaryColor}
|
||||||
|
anchor={20}
|
||||||
|
borderWidth={1.5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnhancedBeamEffect;
|
||||||
|
|
@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import ModalsComponent from "../components/modalsComponent";
|
import ModalsComponent from "../components/modalsComponent";
|
||||||
|
import EmptyPageCommunity from "./empty-page";
|
||||||
import EmptyPage from "./emptyPage";
|
import EmptyPage from "./emptyPage";
|
||||||
|
|
||||||
export default function CollectionPage(): JSX.Element {
|
export default function CollectionPage(): JSX.Element {
|
||||||
|
|
@ -80,7 +81,9 @@ export default function CollectionPage(): JSX.Element {
|
||||||
{flows?.length !== examples?.length || folders?.length > 1 ? (
|
{flows?.length !== examples?.length || folders?.length > 1 ? (
|
||||||
<Outlet />
|
<Outlet />
|
||||||
) : (
|
) : (
|
||||||
<EmptyPage setOpenModal={setOpenModal} />
|
// <EmptyPage setOpenModal={setOpenModal} />
|
||||||
|
|
||||||
|
<EmptyPageCommunity setOpenModal={setOpenModal} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -23,9 +23,9 @@ import { AppWrapperPage } from "./pages/AppWrapperPage";
|
||||||
import { DashboardWrapperPage } from "./pages/DashboardWrapperPage";
|
import { DashboardWrapperPage } from "./pages/DashboardWrapperPage";
|
||||||
import FlowPage from "./pages/FlowPage";
|
import FlowPage from "./pages/FlowPage";
|
||||||
import LoginPage from "./pages/LoginPage";
|
import LoginPage from "./pages/LoginPage";
|
||||||
import CollectionPage from "./pages/MainPage/pages";
|
|
||||||
import FilesPage from "./pages/MainPage/pages/filesPage";
|
import FilesPage from "./pages/MainPage/pages/filesPage";
|
||||||
import HomePage from "./pages/MainPage/pages/homePage";
|
import HomePage from "./pages/MainPage/pages/homePage";
|
||||||
|
import CollectionPage from "./pages/MainPage/pages/main-page";
|
||||||
import SettingsPage from "./pages/SettingsPage";
|
import SettingsPage from "./pages/SettingsPage";
|
||||||
import ApiKeysPage from "./pages/SettingsPage/pages/ApiKeysPage";
|
import ApiKeysPage from "./pages/SettingsPage/pages/ApiKeysPage";
|
||||||
import GeneralPage from "./pages/SettingsPage/pages/GeneralPage";
|
import GeneralPage from "./pages/SettingsPage/pages/GeneralPage";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { getRepoStars } from "../controllers/API";
|
import { getDiscordCount, getRepoStars } from "../controllers/API";
|
||||||
import { DarkStoreType } from "../types/zustand/dark";
|
import { DarkStoreType } from "../types/zustand/dark";
|
||||||
|
|
||||||
const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0;
|
const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0;
|
||||||
|
|
@ -8,6 +8,10 @@ export const useDarkStore = create<DarkStoreType>((set, get) => ({
|
||||||
dark: JSON.parse(window.localStorage.getItem("isDark")!) ?? false,
|
dark: JSON.parse(window.localStorage.getItem("isDark")!) ?? false,
|
||||||
stars: startedStars,
|
stars: startedStars,
|
||||||
version: "",
|
version: "",
|
||||||
|
latestVersion: "",
|
||||||
|
refreshLatestVersion: (v: string) => {
|
||||||
|
set(() => ({ latestVersion: v }));
|
||||||
|
},
|
||||||
setDark: (dark) => {
|
setDark: (dark) => {
|
||||||
set(() => ({ dark: dark }));
|
set(() => ({ dark: dark }));
|
||||||
window.localStorage.setItem("isDark", dark.toString());
|
window.localStorage.setItem("isDark", dark.toString());
|
||||||
|
|
@ -40,4 +44,10 @@ export const useDarkStore = create<DarkStoreType>((set, get) => ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
discordCount: 0,
|
||||||
|
refreshDiscordCount: () => {
|
||||||
|
getDiscordCount().then((res) => {
|
||||||
|
set(() => ({ discordCount: res }));
|
||||||
|
});
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
--round-btn-shadow: #00000063;
|
--round-btn-shadow: #00000063;
|
||||||
--ice: #31a3cc;
|
--ice: #31a3cc;
|
||||||
--selected: #2196f3;
|
--selected: #2196f3;
|
||||||
|
--discord-color: #5765f2;
|
||||||
|
|
||||||
--error-background: #fef2f2;
|
--error-background: #fef2f2;
|
||||||
--error-foreground: #991b1b;
|
--error-foreground: #991b1b;
|
||||||
|
|
@ -194,6 +195,7 @@
|
||||||
--accent-amber: 48, 96%, 89%;
|
--accent-amber: 48, 96%, 89%;
|
||||||
--accent-amber-foreground: 26 90.5% 37.1%;
|
--accent-amber-foreground: 26 90.5% 37.1%;
|
||||||
--red-foreground: 0 90.6% 70.8%;
|
--red-foreground: 0 90.6% 70.8%;
|
||||||
|
--indigo-foreground: 243.7 54.5% 41.4%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|
@ -344,6 +346,8 @@
|
||||||
--hover: #1a202e;
|
--hover: #1a202e;
|
||||||
--disabled-run: #6366f1;
|
--disabled-run: #6366f1;
|
||||||
--selected: #0369a1;
|
--selected: #0369a1;
|
||||||
|
--discord-color: #5765f2;
|
||||||
|
|
||||||
|
|
||||||
--filter-foreground: #eef2ff;
|
--filter-foreground: #eef2ff;
|
||||||
--filter-background: #4e46e599;
|
--filter-background: #4e46e599;
|
||||||
|
|
@ -450,5 +454,6 @@
|
||||||
--accent-amber: 22, 78%, 26%;
|
--accent-amber: 22, 78%, 26%;
|
||||||
--accent-amber-foreground: 45.9 96.7% 64.5%;
|
--accent-amber-foreground: 45.9 96.7% 64.5%;
|
||||||
--red-foreground: 0 72.2% 50.6%;
|
--red-foreground: 0 72.2% 50.6%;
|
||||||
|
--indigo-foreground: 234.5 89.5% 73.9%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,11 @@ export type changeUser = {
|
||||||
is_superuser?: boolean;
|
is_superuser?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
profile_image?: string;
|
profile_image?: string;
|
||||||
|
optins?: {
|
||||||
|
github_starred?: boolean;
|
||||||
|
discord_clicked?: boolean;
|
||||||
|
dialog_dismissed?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type resetPasswordType = {
|
export type resetPasswordType = {
|
||||||
|
|
@ -172,6 +177,11 @@ export type Users = {
|
||||||
profile_image: string;
|
profile_image: string;
|
||||||
create_at: Date;
|
create_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
|
optins?: {
|
||||||
|
github_starred?: boolean;
|
||||||
|
discord_clicked?: boolean;
|
||||||
|
dialog_dismissed?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Component = {
|
export type Component = {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ export type DarkStoreType = {
|
||||||
dark: boolean;
|
dark: boolean;
|
||||||
stars: number;
|
stars: number;
|
||||||
version: string;
|
version: string;
|
||||||
|
latestVersion: string;
|
||||||
setDark: (dark: boolean) => void;
|
setDark: (dark: boolean) => void;
|
||||||
refreshVersion: (v: string) => void;
|
refreshVersion: (v: string) => void;
|
||||||
|
refreshLatestVersion: (v: string) => void;
|
||||||
refreshStars: () => void;
|
refreshStars: () => void;
|
||||||
|
discordCount: number;
|
||||||
|
refreshDiscordCount: () => void;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,7 @@ export const nodeIconToDisplayIconMap: Record<string, string> = {
|
||||||
ScrapeGraphMarkdownifyApi: "ScrapeGraph",
|
ScrapeGraphMarkdownifyApi: "ScrapeGraph",
|
||||||
Unlink: "UnlinkIcon",
|
Unlink: "UnlinkIcon",
|
||||||
note: "StickyNote",
|
note: "StickyNote",
|
||||||
|
TwitterXIcon: "TwitterXIcon",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLucideIconName = (name: string): string => {
|
export const getLucideIconName = (name: string): string => {
|
||||||
|
|
|
||||||
|
|
@ -872,3 +872,15 @@ export const convertUTCToLocalTimezone = (timestamp: string) => {
|
||||||
const localTimezone = moment.tz.guess();
|
const localTimezone = moment.tz.guess();
|
||||||
return moment.utc(timestamp).tz(localTimezone).format("MM/DD/YYYY HH:mm:ss");
|
return moment.utc(timestamp).tz(localTimezone).format("MM/DD/YYYY HH:mm:ss");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatNumber = (num: number | undefined): string => {
|
||||||
|
if (num === undefined) return "0";
|
||||||
|
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return (num / 1000000).toFixed(0) + "M";
|
||||||
|
}
|
||||||
|
if (num >= 1000) {
|
||||||
|
return (num / 1000).toFixed(0) + "k";
|
||||||
|
}
|
||||||
|
return num?.toString();
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,8 @@ const config = {
|
||||||
"slider-input-border": "var(--slider-input-border)",
|
"slider-input-border": "var(--slider-input-border)",
|
||||||
"zinc-foreground": "hsl(var(--zinc-foreground))",
|
"zinc-foreground": "hsl(var(--zinc-foreground))",
|
||||||
"red-foreground": "hsl(var(--red-foreground))",
|
"red-foreground": "hsl(var(--red-foreground))",
|
||||||
|
"indigo-foreground": "hsl(var(--indigo-foreground))",
|
||||||
|
"discord-color": "var(--discord-color)",
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: `var(--radius)`,
|
lg: `var(--radius)`,
|
||||||
|
|
|
||||||
242
src/frontend/tests/core/features/user-progress-track.spec.ts
Normal file
242
src/frontend/tests/core/features/user-progress-track.spec.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { DISCORD_URL, GITHUB_URL } from "../../../src/constants/constants";
|
||||||
|
import { addNewUserAndLogin } from "../../utils/add-new-user-and-loggin";
|
||||||
|
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||||
|
|
||||||
|
test(
|
||||||
|
"admin user must be able to track their progress in getting started",
|
||||||
|
{ tag: ["@release", "@api"] },
|
||||||
|
async ({ page, context }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Wait for any loading text to disappear
|
||||||
|
await page.waitForSelector('text="Loading"', {
|
||||||
|
state: "hidden",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
let emptyButton = page.getByTestId("new_project_btn_empty_page");
|
||||||
|
while ((await emptyButton.count()) === 0) {
|
||||||
|
await page.getByTestId("home-dropdown-menu").first().click();
|
||||||
|
await page.getByTestId("btn_delete_dropdown_menu").first().click();
|
||||||
|
await page
|
||||||
|
.getByTestId("btn_delete_delete_confirmation_modal")
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
emptyButton = page.getByTestId("new_project_btn_empty_page");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(emptyButton).toBeVisible();
|
||||||
|
await expect(page.getByTestId("mainpage_title")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_description")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_github_button")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_discord_button")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("empty_page_drag_and_drop_text"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByTestId("app-header")).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("empty_page_github_button").click();
|
||||||
|
|
||||||
|
const pagePromiseGithub = context.waitForEvent("page");
|
||||||
|
|
||||||
|
const newPageGithub = await pagePromiseGithub;
|
||||||
|
await newPageGithub.waitForTimeout(3000);
|
||||||
|
const newUrlGithub = newPageGithub.url();
|
||||||
|
|
||||||
|
await expect(newUrlGithub).toContain(GITHUB_URL);
|
||||||
|
|
||||||
|
await newPageGithub.close();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("mainpage_title")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_description")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("new_project_btn_empty_page").click();
|
||||||
|
|
||||||
|
await page.getByTestId("side_nav_options_all-templates").click();
|
||||||
|
await page.getByRole("heading", { name: "Basic Prompting" }).click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
|
||||||
|
timeout: 100000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
|
||||||
|
timeout: 100000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByTestId("app-header")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("github_starred_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("create_flow_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("discord_joined_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("get_started_progress_percentage").first(),
|
||||||
|
).toHaveText("66%");
|
||||||
|
|
||||||
|
await page.getByTestId("discord_joined_btn_get_started").click();
|
||||||
|
const pagePromiseDiscord = context.waitForEvent("page");
|
||||||
|
|
||||||
|
const newPageDiscord = await pagePromiseDiscord;
|
||||||
|
await newPageDiscord.waitForTimeout(3000);
|
||||||
|
const newUrlDiscord = newPageDiscord.url();
|
||||||
|
|
||||||
|
await expect(newUrlDiscord).toContain(DISCORD_URL);
|
||||||
|
|
||||||
|
await newPageDiscord.close();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("app-header")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("discord_joined_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("create_flow_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("github_starred_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("get_started_progress_percentage").first(),
|
||||||
|
).toHaveText("100%");
|
||||||
|
|
||||||
|
await page.getByTestId("close_get_started_dialog").click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("discord_joined_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("create_flow_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("github_starred_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"normal user must be able to track their progress in getting started",
|
||||||
|
{ tag: ["@release", "@api"] },
|
||||||
|
async ({ page, context }) => {
|
||||||
|
await addNewUserAndLogin(page);
|
||||||
|
|
||||||
|
// Wait for any loading text to disappear
|
||||||
|
await page.waitForSelector('text="Loading"', {
|
||||||
|
state: "hidden",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
let emptyButton = page.getByTestId("new_project_btn_empty_page");
|
||||||
|
while ((await emptyButton.count()) === 0) {
|
||||||
|
await page.getByTestId("home-dropdown-menu").first().click();
|
||||||
|
await page.getByTestId("btn_delete_dropdown_menu").first().click();
|
||||||
|
await page
|
||||||
|
.getByTestId("btn_delete_delete_confirmation_modal")
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
emptyButton = page.getByTestId("new_project_btn_empty_page");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(emptyButton).toBeVisible();
|
||||||
|
await expect(page.getByTestId("mainpage_title")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_description")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_github_button")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_discord_button")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("empty_page_drag_and_drop_text"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByTestId("app-header")).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("empty_page_github_button").click();
|
||||||
|
|
||||||
|
const pagePromiseGithub = context.waitForEvent("page");
|
||||||
|
|
||||||
|
const newPageGithub = await pagePromiseGithub;
|
||||||
|
await newPageGithub.waitForTimeout(3000);
|
||||||
|
const newUrlGithub = newPageGithub.url();
|
||||||
|
|
||||||
|
await expect(newUrlGithub).toContain(GITHUB_URL);
|
||||||
|
|
||||||
|
await newPageGithub.close();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("mainpage_title")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("empty_page_description")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("new_project_btn_empty_page").click();
|
||||||
|
|
||||||
|
await page.getByTestId("side_nav_options_all-templates").click();
|
||||||
|
await page.getByRole("heading", { name: "Basic Prompting" }).click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
|
||||||
|
timeout: 100000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
|
||||||
|
timeout: 100000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByTestId("app-header")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("github_starred_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("create_flow_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("discord_joined_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("get_started_progress_percentage").first(),
|
||||||
|
).toHaveText("66%");
|
||||||
|
|
||||||
|
await page.getByTestId("discord_joined_btn_get_started").click();
|
||||||
|
const pagePromiseDiscord = context.waitForEvent("page");
|
||||||
|
|
||||||
|
const newPageDiscord = await pagePromiseDiscord;
|
||||||
|
await newPageDiscord.waitForTimeout(3000);
|
||||||
|
const newUrlDiscord = newPageDiscord.url();
|
||||||
|
|
||||||
|
await expect(newUrlDiscord).toContain(DISCORD_URL);
|
||||||
|
|
||||||
|
await newPageDiscord.close();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("app-header")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("discord_joined_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("create_flow_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("github_starred_icon_get_started"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("get_started_progress_percentage").first(),
|
||||||
|
).toHaveText("100%");
|
||||||
|
|
||||||
|
await page.getByTestId("close_get_started_dialog").click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("discord_joined_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("create_flow_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("github_starred_icon_get_started"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
},
|
||||||
|
);
|
||||||
103
src/frontend/tests/utils/add-new-user-and-loggin.ts
Normal file
103
src/frontend/tests/utils/add-new-user-and-loggin.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
export const addNewUserAndLogin = async (page: Page) => {
|
||||||
|
await page.route("**/api/v1/auto_login", (route) => {
|
||||||
|
route.fulfill({
|
||||||
|
status: 500,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
detail: { auto_login: false },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
window.process = window.process || {};
|
||||||
|
|
||||||
|
const newEnv = { ...window.process.env, LANGFLOW_AUTO_LOGIN: "false" };
|
||||||
|
|
||||||
|
Object.defineProperty(window.process, "env", {
|
||||||
|
value: newEnv,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sessionStorage.setItem("testMockAutoLogin", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
const randomName = Math.random().toString(36).substring(5);
|
||||||
|
const randomPassword = Math.random().toString(36).substring(5);
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.waitForSelector("text=sign in to langflow", { timeout: 30000 });
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Username").fill("langflow");
|
||||||
|
await page.getByPlaceholder("Password").fill("langflow");
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
sessionStorage.removeItem("testMockAutoLogin");
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Sign In" }).click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="mainpage_title"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForSelector('[id="new-project-btn"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("user-profile-settings").click();
|
||||||
|
|
||||||
|
await page.getByText("Admin Page", { exact: true }).click();
|
||||||
|
|
||||||
|
//CRUD an user
|
||||||
|
await page.getByText("New User", { exact: true }).click();
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Username").last().fill(randomName);
|
||||||
|
await page.locator('input[name="password"]').fill(randomPassword);
|
||||||
|
await page.locator('input[name="confirmpassword"]').fill(randomPassword);
|
||||||
|
|
||||||
|
await page.waitForSelector("#is_active", {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.locator("#is_active").click();
|
||||||
|
|
||||||
|
await page.getByText("Save", { exact: true }).click();
|
||||||
|
|
||||||
|
await page.waitForSelector("text=new user added", { timeout: 30000 });
|
||||||
|
|
||||||
|
await expect(page.getByText(randomName, { exact: true })).toBeVisible({
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForSelector("[data-testid='user-profile-settings']", {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("user-profile-settings").click();
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
sessionStorage.setItem("testMockAutoLogin", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByText("Logout", { exact: true }).click();
|
||||||
|
|
||||||
|
await page.waitForSelector("text=sign in to langflow", { timeout: 30000 });
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Username").fill(randomName);
|
||||||
|
await page.getByPlaceholder("Password").fill(randomPassword);
|
||||||
|
|
||||||
|
await page.waitForSelector("text=Sign in", {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Sign In" }).click();
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
sessionStorage.removeItem("testMockAutoLogin");
|
||||||
|
});
|
||||||
|
};
|
||||||
56
tasks/20240610_165500_background_gradient_border_styling.md
Normal file
56
tasks/20240610_165500_background_gradient_border_styling.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Task: Gradient Border Styling Refinement
|
||||||
|
|
||||||
|
**Status**: Completed
|
||||||
|
|
||||||
|
## Analysis
|
||||||
|
|
||||||
|
- [x] Requirements
|
||||||
|
- [x] Match the gradient border style of the GitHub card shown in the reference image
|
||||||
|
- [x] Ensure a consistent purple hue that fades from top to bottom
|
||||||
|
- [x] Maintain proper dark background for content
|
||||||
|
- [x] Challenges
|
||||||
|
- [x] Achieving precise gradient fade matching the reference image
|
||||||
|
- [x] Balancing border visibility while maintaining subtle effect
|
||||||
|
- [x] Dependencies
|
||||||
|
- [x] Existing BackgroundGradient component
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
- [x] Step 1: Review the current implementation
|
||||||
|
- [x] Analyze existing gradient colors and opacity values
|
||||||
|
- [x] Compare with reference image for differences
|
||||||
|
- [x] Step 2: Adjust color and gradient parameters
|
||||||
|
- [x] Modify gradient colors to match the purple hue
|
||||||
|
- [x] Fine-tune opacity values for natural fade
|
||||||
|
- [x] Ensure proper background color for inner content
|
||||||
|
- [x] Step 3: Optimize hover effects
|
||||||
|
- [x] Refine blur and opacity transitions on hover
|
||||||
|
- [x] Test with different content types
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
- [x] Modify gradient parameters
|
||||||
|
- [x] Update gradient direction and opacity stops
|
||||||
|
- [x] Simplify to single-color fade for consistency
|
||||||
|
- [x] Adjust container styles
|
||||||
|
- [x] Set proper padding for border thickness
|
||||||
|
- [x] Ensure rounded corners match reference
|
||||||
|
- [x] Test and refine
|
||||||
|
- [x] Compare implementation with reference image
|
||||||
|
- [x] Make final adjustments to achieve exact match
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- [x] Files modified: `src/frontend/src/components/ui/background-gradient.tsx`
|
||||||
|
- [x] Dependencies added/changed: None
|
||||||
|
- [x] Edge cases considered: Different screen sizes, container dimensions
|
||||||
|
- [x] Known limitations: Exact color reproduction may vary slightly based on monitor calibration
|
||||||
|
- [x] Future impact points: Component can be reused across the application for consistent styling
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
1. Changed the gradient to a simpler purple fade from top to bottom
|
||||||
|
2. Used opacity values of 0.7 at top and 0.3 at bottom for subtle fade
|
||||||
|
3. Reduced border thickness to 2px with `p-[2px]`
|
||||||
|
4. Made the hover effect more subtle with `opacity-60` instead of `opacity-100`
|
||||||
|
5. Changed blur on hover from `blur-lg` to `blur-md` for a less intense glow
|
||||||
Loading…
Add table
Add a link
Reference in a new issue