diff --git a/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx b/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx index a3ffdd5..7486651 100644 --- a/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx +++ b/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx @@ -1,6 +1,8 @@ import styled from "styled-components"; export const PlaybookListItem = styled.div` + position: relative; + display: flex; align-items: center; gap: ${({ theme }) => theme.spacing["2xl"]}; @@ -8,20 +10,30 @@ export const PlaybookListItem = styled.div` padding-left: ${({ theme }) => theme.spacing.lg}; padding-right: ${({ theme }) => theme.spacing.lg}; - border: 2px solid ${({ theme }) => theme.colors.palette.primary}; - border-radius: ${({ theme }) => theme.radius.lg}; + border: 1px solid ${({ theme }) => theme.colors.border.medium}; + border-radius: ${({ theme }) => `0 ${theme.radius.sm} ${theme.radius.sm} 0`}; background-color: ${({ theme }) => theme.colors.background.primary}; + &::before { + content: ""; + position: absolute; + top: -1px; + bottom: -1px; + left: -1px; + + width: 4px; + + background-color: ${({ theme }) => theme.colors.palette.primary}; + } + cursor: pointer; transition: background-color ${({ theme }) => theme.transitions.base}, - border-color ${({ theme }) => theme.transitions.base}, - box-shadow ${({ theme }) => theme.transitions.base}; + border-color ${({ theme }) => theme.transitions.base}; &:hover { border-color: ${({ theme }) => theme.colors.info.border}; background-color: ${({ theme }) => theme.colors.info.bg}; - box-shadow: ${({ theme }) => theme.shadows.md}; } `; @@ -144,6 +156,15 @@ export const PlaybookActions = styled.div` padding-left: ${({ theme }) => theme.spacing.md}; `; +export const HeaderActions = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing.md}; + + width: 100%; + max-width: 400px; +`; + export const PlaybookList = styled.div` display: flex; flex-direction: column; diff --git a/src/pages/main-page/playbooks-page/PlaybooksPage.tsx b/src/pages/main-page/playbooks-page/PlaybooksPage.tsx index 13f14da..45d9f16 100644 --- a/src/pages/main-page/playbooks-page/PlaybooksPage.tsx +++ b/src/pages/main-page/playbooks-page/PlaybooksPage.tsx @@ -1,6 +1,6 @@ import { ThemeSize, ThemeVariant } from "@/components"; -import { useQuery } from "@tanstack/react-query"; import { IconFilePlus } from "@tabler/icons-react"; +import { useQuery } from "@tanstack/react-query"; import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router"; @@ -14,13 +14,16 @@ import { CardTitle, CenteredCardContent, Icon, + Input, Spinner, Text, } from "@/components"; import { Playbook, Step } from "@/types"; import { PATHS, sortByString } from "@/utils"; +import { PlaybookActionsMenu } from "./PlaybookActionsMenu"; import { + HeaderActions, PlaybookActions, PlaybookDescription, PlaybookInfo, @@ -34,8 +37,7 @@ import { TimelineStep, TimelineSteps, } from "./PlaybooksPage.styles"; -import { getOrderedSteps } from "./utils"; -import { PlaybookActionsMenu } from "./PlaybookActionsMenu"; +import { filterPlaybooks, getOrderedSteps } from "./utils"; interface PlaybookTimelineProps { workflow: Record; @@ -135,6 +137,7 @@ const PlaybookItem: React.FC = ({ playbook, onClick }) => { export const PlaybooksPage: React.FC = () => { const navigate = useNavigate(); + const [search, setSearch] = useState(""); const { data, isLoading, isError, error } = useQuery({ queryKey: ["playbooks"], queryFn: getPlaybooks, @@ -142,7 +145,7 @@ export const PlaybooksPage: React.FC = () => { }); const sortedPlaybooks = sortByString( - [...(data || [])], + [...filterPlaybooks(data || [], search)], (p) => p.name || "", true, false, @@ -164,7 +167,13 @@ export const PlaybooksPage: React.FC = () => { if (isError) return {parsedError?.message || "Could not load playbooks"}; if (sortedPlaybooks.length === 0) - return No playbooks available; + return ( + + {search.trim() + ? `No playbooks match "${search.trim()}"` + : "No playbooks available"} + + ); return ( {sortedPlaybooks.map((playbook) => ( @@ -182,13 +191,24 @@ export const PlaybooksPage: React.FC = () => { Playbooks - + + setSearch(e.target.value)} + /> + + {renderBody()} diff --git a/src/pages/main-page/playbooks-page/utils.ts b/src/pages/main-page/playbooks-page/utils.ts index 5af8a5a..d145401 100644 --- a/src/pages/main-page/playbooks-page/utils.ts +++ b/src/pages/main-page/playbooks-page/utils.ts @@ -101,6 +101,22 @@ export const getOrderedSteps = ( return steps; }; +/** + * Case-insensitive search on name, id and playbook types. + */ +export const filterPlaybooks = ( + playbooks: Playbook[], + query: string, +): Playbook[] => { + const q = query.trim().toLowerCase(); + if (!q) return playbooks; + return playbooks.filter((p) => + [p.name, p.id, ...(p.playbook_types ?? [])].some((field) => + field?.toLowerCase().includes(q), + ), + ); +}; + /** * Generates a safe filename for exporting a playbook. * Format: {id}-{modified-date}.json