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 typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
from langflow.schema.serialize import UUIDstr
|
||||
|
|
@ -13,6 +15,13 @@ if TYPE_CHECKING:
|
|||
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]
|
||||
id: UUIDstr = Field(default_factory=uuid4, primary_key=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",
|
||||
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):
|
||||
username: str = Field()
|
||||
password: str = Field()
|
||||
optins: dict[str, Any] | None = Field(
|
||||
default={"github_starred": False, "dialog_dismissed": False, "discord_clicked": False}
|
||||
)
|
||||
|
||||
|
||||
class UserRead(SQLModel):
|
||||
|
|
@ -54,6 +69,7 @@ class UserRead(SQLModel):
|
|||
create_at: datetime = Field()
|
||||
updated_at: datetime = Field()
|
||||
last_login_at: datetime | None = Field(nullable=True)
|
||||
optins: dict[str, Any] | None = Field(default=None)
|
||||
|
||||
|
||||
class UserUpdate(SQLModel):
|
||||
|
|
@ -63,3 +79,4 @@ class UserUpdate(SQLModel):
|
|||
is_active: bool | None = None
|
||||
is_superuser: bool | None = None
|
||||
last_login_at: datetime | None = None
|
||||
optins: dict[str, Any] | None = None
|
||||
|
|
|
|||
|
|
@ -74,8 +74,6 @@ const RenderInputParameters = ({
|
|||
return keyMap;
|
||||
}, [templateFields, data.id, data.node?.template]);
|
||||
|
||||
console.log({ data });
|
||||
|
||||
const renderInputParameter = templateFields.map(
|
||||
(templateField: string, idx) => {
|
||||
const template = data.node?.template[templateField];
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const AlertDropdown = forwardRef<HTMLDivElement, AlertDropdownType>(
|
|||
<PopoverContent
|
||||
ref={notificationRef}
|
||||
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">
|
||||
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 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 useAuthStore from "@/stores/authStore";
|
||||
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 GithubStarComponent from "../GithubStarButton";
|
||||
import {
|
||||
HeaderMenu,
|
||||
HeaderMenuItemButton,
|
||||
HeaderMenuItemLink,
|
||||
HeaderMenuItems,
|
||||
HeaderMenuItemsSection,
|
||||
HeaderMenuToggle,
|
||||
} from "../HeaderMenu";
|
||||
import { ProfileIcon } from "../ProfileIcon";
|
||||
import ThemeButtons from "../ThemeButtons";
|
||||
|
||||
export const AccountMenu = () => {
|
||||
const [isFeedbackOpen, setIsFeedbackOpen] = useState(false);
|
||||
const [isCustomFeatureFlagsOpen, setIsCustomFeatureFlagsOpen] =
|
||||
useState(false);
|
||||
const { customParam: id } = useParams();
|
||||
const version = useDarkStore((state) => state.version);
|
||||
const latestVersion = useDarkStore((state) => state.latestVersion);
|
||||
const navigate = useCustomNavigate();
|
||||
const { mutate: mutationLogout } = useLogout();
|
||||
|
||||
|
|
@ -40,6 +38,8 @@ export const AccountMenu = () => {
|
|||
mutationLogout();
|
||||
};
|
||||
|
||||
const isLatestVersion = version === latestVersion;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderMenu>
|
||||
|
|
@ -48,34 +48,37 @@ export const AccountMenu = () => {
|
|||
className="h-7 w-7 rounded-lg focus-visible:outline-0"
|
||||
data-testid="user-profile-settings"
|
||||
>
|
||||
{ENABLE_DATASTAX_LANGFLOW ? <CustomProfileIcon /> : <ProfileIcon />}
|
||||
<ProfileIcon />
|
||||
</div>
|
||||
</HeaderMenuToggle>
|
||||
<HeaderMenuItems position="right">
|
||||
{ENABLE_DATASTAX_LANGFLOW && (
|
||||
<HeaderMenuItemsSection>
|
||||
<CustomHeaderMenuItemsTitle />
|
||||
</HeaderMenuItemsSection>
|
||||
)}
|
||||
<HeaderMenuItemsSection>
|
||||
<div className="flex h-[46px] w-full items-center justify-between px-3">
|
||||
<div className="pl-1 text-xs text-zinc-500">
|
||||
<span
|
||||
data-testid="menu_version_button"
|
||||
id="menu_version_button"
|
||||
>
|
||||
Version {version}
|
||||
</span>
|
||||
<HeaderMenuItems position="right" classNameSize="w-[272px]">
|
||||
<div className="divide-y divide-foreground/10">
|
||||
<div>
|
||||
<div className="h-[46px] items-center px-4 pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span
|
||||
data-testid="menu_version_button"
|
||||
id="menu_version_button"
|
||||
className="text-sm"
|
||||
>
|
||||
Version
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
"float-right text-xs",
|
||||
isLatestVersion && "text-accent-emerald-foreground",
|
||||
!isLatestVersion && "text-accent-amber-foreground",
|
||||
)}
|
||||
>
|
||||
{version}{" "}
|
||||
{isLatestVersion ? "(latest)" : "(update available)"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!ENABLE_DATASTAX_LANGFLOW && <ThemeButtons />}
|
||||
</div>
|
||||
{ENABLE_DATASTAX_LANGFLOW ? (
|
||||
<HeaderMenuItemLink newPage href={`/settings/org/${id}/overview`}>
|
||||
Account Settings
|
||||
</HeaderMenuItemLink>
|
||||
) : (
|
||||
|
||||
<div>
|
||||
<HeaderMenuItemButton
|
||||
icon="arrow-right"
|
||||
onClick={() => {
|
||||
navigate("/settings");
|
||||
}}
|
||||
|
|
@ -87,100 +90,84 @@ export const AccountMenu = () => {
|
|||
Settings
|
||||
</span>
|
||||
</HeaderMenuItemButton>
|
||||
)}
|
||||
{!ENABLE_DATASTAX_LANGFLOW && (
|
||||
<>
|
||||
{isAdmin && !autoLogin && (
|
||||
<HeaderMenuItemButton onClick={() => navigate("/admin")}>
|
||||
|
||||
{isAdmin && !autoLogin && (
|
||||
<div>
|
||||
<HeaderMenuItemButton
|
||||
onClick={() => {
|
||||
navigate("/admin");
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-testid="menu_admin_button"
|
||||
id="menu_admin_button"
|
||||
data-testid="menu_admin_page_button"
|
||||
id="menu_admin_page_button"
|
||||
>
|
||||
Admin Page
|
||||
</span>
|
||||
</HeaderMenuItemButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{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">
|
||||
</div>
|
||||
)}
|
||||
<HeaderMenuItemLink newPage href={DOCS_URL}>
|
||||
<span data-testid="menu_docs_button" id="menu_docs_button">
|
||||
Docs
|
||||
</span>
|
||||
</HeaderMenuItemLink>
|
||||
)}
|
||||
</HeaderMenuItemsSection>
|
||||
<HeaderMenuItemsSection>
|
||||
{ENABLE_DATASTAX_LANGFLOW ? (
|
||||
<HeaderMenuItemLink
|
||||
newPage
|
||||
href="https://github.com/langflow-ai/langflow"
|
||||
>
|
||||
<div className="-my-2 mr-2 flex w-full items-center justify-between">
|
||||
<div className="text-sm">Star the repo</div>
|
||||
<GithubStarComponent />
|
||||
</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
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<HeaderMenuItemLink newPage href={GITHUB_URL}>
|
||||
<span
|
||||
data-testid="menu_github_button"
|
||||
id="menu_github_button"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<FaGithub className="h-4 w-4" />
|
||||
GitHub
|
||||
</span>
|
||||
</HeaderMenuItemLink>
|
||||
)}
|
||||
<HeaderMenuItemLink newPage href="https://twitter.com/langflow_ai">
|
||||
<span data-testid="menu_twitter_button" id="menu_twitter_button">
|
||||
Follow Langflow on X
|
||||
</span>
|
||||
</HeaderMenuItemLink>
|
||||
<HeaderMenuItemLink newPage href="https://discord.gg/EqksyE2EX9">
|
||||
<span data-testid="menu_discord_button" id="menu_discord_button">
|
||||
Join the Langflow Discord
|
||||
</span>
|
||||
</HeaderMenuItemLink>
|
||||
</HeaderMenuItemsSection>
|
||||
{ENABLE_DATASTAX_LANGFLOW ? (
|
||||
<HeaderMenuItemsSection>
|
||||
<HeaderMenuItemLink href="/session/logout" icon="log-out">
|
||||
Logout
|
||||
<HeaderMenuItemLink newPage href={DISCORD_URL}>
|
||||
<span
|
||||
data-testid="menu_discord_button"
|
||||
id="menu_discord_button"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<FaDiscord className="h-4 w-4 text-[#5865F2]" />
|
||||
Discord
|
||||
</span>
|
||||
</HeaderMenuItemLink>
|
||||
</HeaderMenuItemsSection>
|
||||
) : (
|
||||
!autoLogin && (
|
||||
<HeaderMenuItemsSection>
|
||||
<HeaderMenuItemLink newPage href={TWITTER_URL}>
|
||||
<span
|
||||
data-testid="menu_twitter_button"
|
||||
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">
|
||||
Logout
|
||||
</HeaderMenuItemButton>
|
||||
</HeaderMenuItemsSection>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</HeaderMenuItems>
|
||||
</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">
|
||||
{children}
|
||||
<ChevronsUpDown
|
||||
className="text-muted-foreground group-hover:text-foreground"
|
||||
size={"15px"}
|
||||
strokeWidth={"2px"}
|
||||
/>
|
||||
<ChevronsUpDown className="h-4 w-4 text-muted-foreground group-hover:text-foreground" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
);
|
||||
|
|
@ -72,10 +68,14 @@ export const HeaderMenuItemButton = ({ icon = "", onClick, children }) => (
|
|||
export const HeaderMenuItems = ({
|
||||
position = "left",
|
||||
children,
|
||||
}: React.PropsWithChildren<{ position?: "left" | "right" }>) => {
|
||||
classNameSize = "w-[20rem]",
|
||||
}: React.PropsWithChildren<{
|
||||
position?: "left" | "right";
|
||||
classNameSize?: string;
|
||||
}>) => {
|
||||
const positionClass = position === "left" ? "left-0" : "right-0";
|
||||
return (
|
||||
<DropdownMenuContent className={cn("w-[20rem]", positionClass)}>
|
||||
<DropdownMenuContent className={cn(classNameSize, positionClass)}>
|
||||
{children}
|
||||
</DropdownMenuContent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export const ThemeButtons = () => {
|
|||
data-testid="menu_light_button"
|
||||
id="menu_light_button"
|
||||
>
|
||||
<ForwardedIconComponent name="Sun" className="w-4" />
|
||||
<ForwardedIconComponent strokeWidth={2} name="Sun" className="w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Dark Theme Button */}
|
||||
|
|
@ -68,14 +68,14 @@ export const ThemeButtons = () => {
|
|||
unstyled
|
||||
className={`relative z-10 mx-1 inline-flex items-center rounded-full px-1 ${
|
||||
selectedTheme === "dark"
|
||||
? "text-background dark:hover:bg-purple-400"
|
||||
: "text-foreground hover:bg-purple-400 hover:text-background"
|
||||
? "bg-indigo-foreground text-primary hover:bg-indigo-foreground"
|
||||
: "text-foreground hover:bg-indigo-foreground hover:text-background"
|
||||
}`}
|
||||
onClick={() => handleThemeChange("dark")}
|
||||
data-testid="menu_dark_button"
|
||||
id="menu_dark_button"
|
||||
>
|
||||
<ForwardedIconComponent name="Moon" className="w-4" />
|
||||
<ForwardedIconComponent strokeWidth={2} name="Moon" className="w-4" />
|
||||
</Button>
|
||||
|
||||
{/* System Theme Button */}
|
||||
|
|
@ -90,7 +90,11 @@ export const ThemeButtons = () => {
|
|||
data-testid="menu_system_button"
|
||||
id="menu_system_button"
|
||||
>
|
||||
<ForwardedIconComponent name="Monitor" className="w-4" />
|
||||
<ForwardedIconComponent
|
||||
name="Monitor"
|
||||
className="w-4"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Button>
|
||||
</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 { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AccountMenu } from "./components/AccountMenu";
|
||||
import FlowMenu from "./components/FlowMenu";
|
||||
import GithubStarComponent from "./components/GithubStarButton";
|
||||
import LangflowCounts from "./components/langflow-counts";
|
||||
|
||||
export default function AppHeader(): JSX.Element {
|
||||
const notificationCenter = useAlertStore((state) => state.notificationCenter);
|
||||
|
|
@ -46,9 +48,17 @@ export default function AppHeader(): JSX.Element {
|
|||
|
||||
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 (
|
||||
<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"
|
||||
>
|
||||
{/* Left Section */}
|
||||
|
|
@ -83,7 +93,7 @@ export default function AppHeader(): JSX.Element {
|
|||
|
||||
{/* Right Section */}
|
||||
<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"
|
||||
>
|
||||
{!ENABLE_DATASTAX_LANGFLOW && (
|
||||
|
|
@ -95,7 +105,7 @@ export default function AppHeader(): JSX.Element {
|
|||
window.open("https://github.com/langflow-ai/langflow", "_blank")
|
||||
}
|
||||
>
|
||||
<GithubStarComponent />
|
||||
<LangflowCounts />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -111,8 +121,7 @@ export default function AppHeader(): JSX.Element {
|
|||
<AlertDropdown onClose={() => setActiveState(null)}>
|
||||
<Button
|
||||
ref={notificationRef}
|
||||
variant="ghost"
|
||||
className={`relative ${activeState === "notifications" ? "bg-accent text-accent-foreground" : ""}`}
|
||||
unstyled
|
||||
onClick={() =>
|
||||
setActiveState((prev) =>
|
||||
prev === "notifications" ? null : "notifications",
|
||||
|
|
@ -123,47 +132,24 @@ export default function AppHeader(): JSX.Element {
|
|||
<span
|
||||
className={
|
||||
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"
|
||||
}
|
||||
/>
|
||||
<ForwardedIconComponent
|
||||
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>
|
||||
</Button>
|
||||
</AlertDropdown>
|
||||
</ShadTooltip>
|
||||
</AlertDropdown>
|
||||
{!ENABLE_DATASTAX_LANGFLOW && (
|
||||
<>
|
||||
<ShadTooltip
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="my-auto ml-3 h-7 dark:border-zinc-700"
|
||||
/>
|
||||
{ENABLE_DATASTAX_LANGFLOW && (
|
||||
<>
|
||||
<ShadTooltip content="Docs" side="bottom" styleClasses="z-10">
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export default function CardsWrapComponent({
|
|||
className={cn(
|
||||
"h-full w-full",
|
||||
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 { GetStartedProgress } from "@/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/get-started-progress";
|
||||
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 { UploadFolderButton } from "./upload-folder-button";
|
||||
|
||||
|
|
@ -13,23 +17,54 @@ export const HeaderButtons = ({
|
|||
isUpdatingFolder: boolean;
|
||||
isPending: boolean;
|
||||
addNewFolder: () => void;
|
||||
}) => (
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<SidebarTrigger className="lg:hidden">
|
||||
<IconComponent name="PanelLeftClose" className="h-4 w-4" />
|
||||
</SidebarTrigger>
|
||||
}) => {
|
||||
const userData = useAuthStore((state) => state.userData);
|
||||
const userDismissedDialog = userData?.optins?.dialog_dismissed;
|
||||
const isGithubStarred = userData?.optins?.github_starred;
|
||||
const isDiscordJoined = userData?.optins?.discord_clicked;
|
||||
|
||||
<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>
|
||||
);
|
||||
const [isDismissedDialog, setIsDismissedDialog] =
|
||||
useState(userDismissedDialog);
|
||||
|
||||
const handleDismissDialog = () => {
|
||||
setIsDismissedDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isDismissedDialog && (
|
||||
<>
|
||||
<GetStartedProgress
|
||||
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)))";
|
||||
|
||||
export const RECEIVING_INPUT_VALUE = "Receiving input";
|
||||
export const SELECT_AN_OPTION = "Select an option...";
|
||||
|
||||
export const ICON_STROKE_WIDTH = 1.5;
|
||||
|
||||
|
|
@ -1068,5 +1069,9 @@ export const OPENAI_VOICES = [
|
|||
export const DEFAULT_POLLING_INTERVAL = 5000;
|
||||
export const DEFAULT_TIMEOUT = 30000;
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { FlowStyleType, FlowType } from "../../types/flow";
|
|||
import { StoreComponentResponse } from "../../types/store";
|
||||
|
||||
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) {
|
||||
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) {
|
||||
try {
|
||||
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 { useMutationFunctionType, Users } from "../../../../types/api";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
||||
export const useGetUserData: useMutationFunctionType<undefined, any> = (
|
||||
options?,
|
||||
) => {
|
||||
const setUserData = useAuthStore((state) => state.setUserData);
|
||||
const { mutate } = UseRequestProcessor();
|
||||
|
||||
const getUserData = async () => {
|
||||
const response = await api.get<Users>(`${getURL("USERS")}/whoami`);
|
||||
setUserData(response["data"]);
|
||||
return response["data"];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { UseRequestProcessor } from "../../services/request-processor";
|
|||
interface versionQueryResponse {
|
||||
version: string;
|
||||
package: string;
|
||||
main_version: string;
|
||||
}
|
||||
|
||||
export const useGetVersionQuery: useQueryFunctionType<
|
||||
|
|
@ -22,7 +23,9 @@ export const useGetVersionQuery: useQueryFunctionType<
|
|||
const responseFn = async () => {
|
||||
const { data } = await getVersionFn();
|
||||
const refreshVersion = useDarkStore.getState().refreshVersion;
|
||||
const refreshLatestVersion = useDarkStore.getState().refreshLatestVersion;
|
||||
refreshVersion(data.version);
|
||||
refreshLatestVersion(data.main_version);
|
||||
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,
|
||||
ZoomOut,
|
||||
} from "lucide-react";
|
||||
import { TwitterXIcon } from "./Twitter X";
|
||||
|
||||
// Create a map of eagerly loaded Lucide icons
|
||||
export const lucideIcons = {
|
||||
|
|
@ -464,4 +465,5 @@ export const lucideIcons = {
|
|||
Zap,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
TwitterXIcon,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export default function DeleteConfirmationModal({
|
|||
onClick={(e) => e.stopPropagation()}
|
||||
className="mr-1"
|
||||
variant="outline"
|
||||
data-testid="btn_cancel_delete_confirmation_modal"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -72,6 +73,7 @@ export default function DeleteConfirmationModal({
|
|||
onClick={(e) => {
|
||||
onConfirm(e);
|
||||
}}
|
||||
data-testid="btn_delete_delete_confirmation_modal"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import { LoadingPage } from "../LoadingPage";
|
|||
|
||||
export function AppInitPage() {
|
||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||
const refreshDiscordCount = useDarkStore(
|
||||
(state) => state.refreshDiscordCount,
|
||||
);
|
||||
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
||||
|
||||
const { isFetched: isLoaded } = useCustomPrimaryLoading();
|
||||
|
|
@ -31,6 +34,7 @@ export function AppInitPage() {
|
|||
useEffect(() => {
|
||||
if (isFetched) {
|
||||
refreshStars();
|
||||
refreshDiscordCount();
|
||||
}
|
||||
|
||||
if (isConfigFetched) {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ const DropdownComponent = ({
|
|||
setOpenDelete(true);
|
||||
}}
|
||||
className="cursor-pointer text-destructive"
|
||||
data-testid="btn_delete_dropdown_menu"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
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 { Outlet } from "react-router-dom";
|
||||
import ModalsComponent from "../components/modalsComponent";
|
||||
import EmptyPageCommunity from "./empty-page";
|
||||
import EmptyPage from "./emptyPage";
|
||||
|
||||
export default function CollectionPage(): JSX.Element {
|
||||
|
|
@ -80,7 +81,9 @@ export default function CollectionPage(): JSX.Element {
|
|||
{flows?.length !== examples?.length || folders?.length > 1 ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<EmptyPage setOpenModal={setOpenModal} />
|
||||
// <EmptyPage setOpenModal={setOpenModal} />
|
||||
|
||||
<EmptyPageCommunity setOpenModal={setOpenModal} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -23,9 +23,9 @@ import { AppWrapperPage } from "./pages/AppWrapperPage";
|
|||
import { DashboardWrapperPage } from "./pages/DashboardWrapperPage";
|
||||
import FlowPage from "./pages/FlowPage";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import CollectionPage from "./pages/MainPage/pages";
|
||||
import FilesPage from "./pages/MainPage/pages/filesPage";
|
||||
import HomePage from "./pages/MainPage/pages/homePage";
|
||||
import CollectionPage from "./pages/MainPage/pages/main-page";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import ApiKeysPage from "./pages/SettingsPage/pages/ApiKeysPage";
|
||||
import GeneralPage from "./pages/SettingsPage/pages/GeneralPage";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from "zustand";
|
||||
import { getRepoStars } from "../controllers/API";
|
||||
import { getDiscordCount, getRepoStars } from "../controllers/API";
|
||||
import { DarkStoreType } from "../types/zustand/dark";
|
||||
|
||||
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,
|
||||
stars: startedStars,
|
||||
version: "",
|
||||
latestVersion: "",
|
||||
refreshLatestVersion: (v: string) => {
|
||||
set(() => ({ latestVersion: v }));
|
||||
},
|
||||
setDark: (dark) => {
|
||||
set(() => ({ dark: dark }));
|
||||
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;
|
||||
--ice: #31a3cc;
|
||||
--selected: #2196f3;
|
||||
--discord-color: #5765f2;
|
||||
|
||||
--error-background: #fef2f2;
|
||||
--error-foreground: #991b1b;
|
||||
|
|
@ -194,6 +195,7 @@
|
|||
--accent-amber: 48, 96%, 89%;
|
||||
--accent-amber-foreground: 26 90.5% 37.1%;
|
||||
--red-foreground: 0 90.6% 70.8%;
|
||||
--indigo-foreground: 243.7 54.5% 41.4%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -344,6 +346,8 @@
|
|||
--hover: #1a202e;
|
||||
--disabled-run: #6366f1;
|
||||
--selected: #0369a1;
|
||||
--discord-color: #5765f2;
|
||||
|
||||
|
||||
--filter-foreground: #eef2ff;
|
||||
--filter-background: #4e46e599;
|
||||
|
|
@ -450,5 +454,6 @@
|
|||
--accent-amber: 22, 78%, 26%;
|
||||
--accent-amber-foreground: 45.9 96.7% 64.5%;
|
||||
--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;
|
||||
password?: string;
|
||||
profile_image?: string;
|
||||
optins?: {
|
||||
github_starred?: boolean;
|
||||
discord_clicked?: boolean;
|
||||
dialog_dismissed?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type resetPasswordType = {
|
||||
|
|
@ -172,6 +177,11 @@ export type Users = {
|
|||
profile_image: string;
|
||||
create_at: Date;
|
||||
updated_at: Date;
|
||||
optins?: {
|
||||
github_starred?: boolean;
|
||||
discord_clicked?: boolean;
|
||||
dialog_dismissed?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type Component = {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ export type DarkStoreType = {
|
|||
dark: boolean;
|
||||
stars: number;
|
||||
version: string;
|
||||
latestVersion: string;
|
||||
setDark: (dark: boolean) => void;
|
||||
refreshVersion: (v: string) => void;
|
||||
refreshLatestVersion: (v: string) => void;
|
||||
refreshStars: () => void;
|
||||
discordCount: number;
|
||||
refreshDiscordCount: () => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ export const nodeIconToDisplayIconMap: Record<string, string> = {
|
|||
ScrapeGraphMarkdownifyApi: "ScrapeGraph",
|
||||
Unlink: "UnlinkIcon",
|
||||
note: "StickyNote",
|
||||
TwitterXIcon: "TwitterXIcon",
|
||||
};
|
||||
|
||||
export const getLucideIconName = (name: string): string => {
|
||||
|
|
|
|||
|
|
@ -872,3 +872,15 @@ export const convertUTCToLocalTimezone = (timestamp: string) => {
|
|||
const localTimezone = moment.tz.guess();
|
||||
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)",
|
||||
"zinc-foreground": "hsl(var(--zinc-foreground))",
|
||||
"red-foreground": "hsl(var(--red-foreground))",
|
||||
"indigo-foreground": "hsl(var(--indigo-foreground))",
|
||||
"discord-color": "var(--discord-color)",
|
||||
},
|
||||
borderRadius: {
|
||||
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