11"use client" ;
22
3- import { ChangeEvent , useRef , useState } from "react" ;
3+ import {
4+ ChangeEvent ,
5+ useCallback ,
6+ useEffect ,
7+ useMemo ,
8+ useRef ,
9+ useState ,
10+ } from "react" ;
411import { Subtitle , Button } from "@tremor/react" ;
512import {
613 ArrowUpOnSquareStackIcon ,
714 PlusCircleIcon ,
815} from "@heroicons/react/24/outline" ;
9- import { KeepLoader , PageTitle } from "@/shared/ui" ;
16+ import { EmptyStateCard , KeepLoader , PageTitle } from "@/shared/ui" ;
1017import WorkflowsEmptyState from "./noworkflows" ;
1118import WorkflowTile from "./workflow-tile" ;
1219import { ArrowRightIcon } from "@radix-ui/react-icons" ;
@@ -16,10 +23,25 @@ import { WorkflowTemplates } from "./mockworkflows";
1623import { useApi } from "@/shared/lib/hooks/useApi" ;
1724import { Input , ErrorComponent } from "@/shared/ui" ;
1825import { Textarea } from "@/components/ui" ;
19- import { useWorkflowsV2 } from "utils/hooks/useWorkflowsV2" ;
26+ import { useWorkflowsV2 , WorkflowsQuery } from "utils/hooks/useWorkflowsV2" ;
2027import { useWorkflowActions } from "@/entities/workflows/model/useWorkflowActions" ;
2128import { PageSubtitle } from "@/shared/ui/PageSubtitle" ;
2229import { PlusIcon } from "@heroicons/react/20/solid" ;
30+ import { UserStatefulAvatar } from "@/entities/users/ui" ;
31+ import { FacetsConfig } from "@/features/filter/models" ;
32+ import {
33+ CheckCircleIcon ,
34+ XCircleIcon ,
35+ ExclamationCircleIcon ,
36+ MagnifyingGlassIcon ,
37+ FunnelIcon ,
38+ ArrowPathIcon ,
39+ } from "@heroicons/react/24/outline" ;
40+ import { useUser } from "@/entities/users/model/useUser" ;
41+ import { Pagination , SearchInput } from "@/features/filter" ;
42+ import { FacetsPanelServerSide } from "@/features/filter/facet-panel-server-side" ;
43+ import { InitialFacetsData } from "@/features/filter/api" ;
44+ import { v4 as uuidV4 } from "uuid" ;
2345
2446const EXAMPLE_WORKFLOW_DEFINITIONS = {
2547 slack : `
@@ -62,11 +84,58 @@ const EXAMPLE_WORKFLOW_DEFINITIONS = {
6284
6385type ExampleWorkflowKey = keyof typeof EXAMPLE_WORKFLOW_DEFINITIONS ;
6486
65- export default function WorkflowsPage ( ) {
87+ const AssigneeLabel = ( { email } : { email : string } ) => {
88+ const user = useUser ( email ) ;
89+ return user ? user . name : email ;
90+ } ;
91+
92+ export default function WorkflowsPage ( {
93+ initialFacetsData,
94+ } : {
95+ initialFacetsData ?: InitialFacetsData ;
96+ } ) {
6697 const router = useRouter ( ) ;
6798 const fileInputRef = useRef < HTMLInputElement | null > ( null ) ;
6899 const [ workflowDefinition , setWorkflowDefinition ] = useState ( "" ) ;
69100 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
101+ const [ clearFiltersToken , setClearFiltersToken ] = useState < string | null > (
102+ null
103+ ) ;
104+ const [ filterCel , setFilterCel ] = useState < string | null > ( null ) ;
105+ const [ searchedValue , setSearchedValue ] = useState < string | null > ( null ) ;
106+ const [ paginationState , setPaginationState ] = useState < {
107+ offset : number ;
108+ limit : number ;
109+ } | null > ( null ) ;
110+ const [ workflowsQuery , setWorkflowsQuery ] = useState < WorkflowsQuery | null > (
111+ null
112+ ) ;
113+
114+ const searchCel = useMemo ( ( ) => {
115+ if ( ! searchedValue ) {
116+ return ;
117+ }
118+
119+ return `name.contains("${ searchedValue } ") || description.contains("${ searchedValue } ")` ;
120+ } , [ searchedValue ] ) ;
121+
122+ useEffect ( ( ) => {
123+ if ( ! paginationState ) {
124+ return ;
125+ }
126+
127+ const celList = [ searchCel , filterCel ] . filter ( ( cel ) => cel ) ;
128+ const cel = celList . join ( " && " ) ;
129+ const query : WorkflowsQuery = {
130+ cel,
131+ limit : paginationState ?. limit ,
132+ offset : paginationState ?. offset ,
133+ sortBy : "createdAt" ,
134+ sortDir : "desc" ,
135+ } ;
136+
137+ setWorkflowsQuery ( query ) ;
138+ } , [ searchCel , filterCel , paginationState ] ) ;
70139
71140 // Only fetch data when the user is authenticated
72141 /**
@@ -81,15 +150,151 @@ export default function WorkflowsPage() {
81150 -> last_executions: Used for the workflow execution graph.
82151 ->last_execution_started: Used for showing the start time of execution in real-time.
83152 **/
84- const { workflows, error, isLoading } = useWorkflowsV2 ( ) ;
153+
154+ const {
155+ workflows : filteredWorkflows ,
156+ totalCount : filteredWorkflowsCount ,
157+ error,
158+ isLoading : isFilteredWorkflowsLoading ,
159+ } = useWorkflowsV2 ( workflowsQuery , { keepPreviousData : true } ) ;
160+
161+ const isFirstLoading = isFilteredWorkflowsLoading && ! filteredWorkflows ;
162+
85163 const { uploadWorkflowFiles } = useWorkflowActions ( ) ;
86164
87- if ( error ) {
88- return < ErrorComponent error = { error } reset = { ( ) => { } } /> ;
165+ const isTableEmpty = filteredWorkflowsCount === 0 ;
166+ const isEmptyState =
167+ ! isFilteredWorkflowsLoading && isTableEmpty && ! workflowsQuery ?. cel ;
168+
169+ const showFilterEmptyState = isTableEmpty && ! ! filterCel ;
170+ const showSearchEmptyState =
171+ isTableEmpty && ! ! searchCel && ! showFilterEmptyState ;
172+
173+ const setPaginationStateCallback = useCallback (
174+ ( pageIndex : number , limit : number , offset : number ) => {
175+ setPaginationState ( { limit, offset } ) ;
176+ } ,
177+ [ setPaginationState ]
178+ ) ;
179+
180+ const facetsConfig : FacetsConfig = useMemo ( ( ) => {
181+ return {
182+ [ "Status" ] : {
183+ renderOptionIcon : ( facetOption ) => {
184+ switch ( facetOption . value ) {
185+ case "success" : {
186+ return < CheckCircleIcon className = "w-5 h-5 text-green-500" /> ;
187+ }
188+ case "failed" : {
189+ return < XCircleIcon className = "w-5 h-5 text-red-500" /> ;
190+ }
191+ case "in_progress" : {
192+ return < ArrowPathIcon className = "w-5 h-5 text-orange-500" /> ;
193+ }
194+ default : {
195+ return (
196+ < ExclamationCircleIcon className = "w-5 h-5 text-gray-500" />
197+ ) ;
198+ }
199+ }
200+ } ,
201+ renderOptionLabel : ( facetOption ) => {
202+ switch ( facetOption . value ) {
203+ case "success" : {
204+ return "Success" ;
205+ }
206+ case "failed" : {
207+ return "Failed" ;
208+ }
209+ case "in_progress" : {
210+ return "In progress" ;
211+ }
212+ default : {
213+ return "Not run yet" ;
214+ }
215+ }
216+ } ,
217+ } ,
218+ [ "Created by" ] : {
219+ renderOptionIcon : ( facetOption ) => (
220+ < UserStatefulAvatar email = { facetOption . display_name } size = "xs" />
221+ ) ,
222+ renderOptionLabel : ( facetOption ) => {
223+ if ( facetOption . display_name === "null" ) {
224+ return "Not assigned" ;
225+ }
226+ return < AssigneeLabel email = { facetOption . display_name } /> ;
227+ } ,
228+ } ,
229+ [ "Enabling status" ] : {
230+ renderOptionLabel : ( facetOption ) =>
231+ facetOption . display_name . toLocaleLowerCase ( ) === "true"
232+ ? "Disabled"
233+ : "Enabled" ,
234+ } ,
235+ } ;
236+ } , [ ] ) ;
237+
238+ function renderFilterEmptyState ( ) {
239+ return (
240+ < >
241+ < div className = "flex items-center h-full w-full" >
242+ < div className = "flex flex-col justify-center items-center w-full p-4" >
243+ < EmptyStateCard
244+ title = "No workflows to display matching your filter"
245+ icon = { FunnelIcon }
246+ >
247+ < Button
248+ color = "orange"
249+ variant = "secondary"
250+ onClick = { ( ) => setClearFiltersToken ( uuidV4 ( ) ) }
251+ >
252+ Reset filter
253+ </ Button >
254+ </ EmptyStateCard >
255+ </ div >
256+ </ div >
257+ </ >
258+ ) ;
259+ }
260+
261+ function renderSearchEmptyState ( ) {
262+ return (
263+ < >
264+ < div className = "flex items-center h-full w-full" >
265+ < div className = "flex flex-col justify-center items-center w-full p-4" >
266+ < EmptyStateCard
267+ title = "No workflows to display matching your search"
268+ icon = { MagnifyingGlassIcon }
269+ >
270+ < Button
271+ color = "orange"
272+ variant = "secondary"
273+ onClick = { ( ) => setSearchedValue ( null ) }
274+ >
275+ Clear search
276+ </ Button >
277+ </ EmptyStateCard >
278+ </ div >
279+ </ div >
280+ </ >
281+ ) ;
89282 }
90283
91- if ( isLoading || ! workflows ) {
92- return < KeepLoader /> ;
284+ function renderData ( ) {
285+ return (
286+ < >
287+ < div className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 w-full gap-4" >
288+ { filteredWorkflows ?. map ( ( workflow ) => (
289+ < WorkflowTile key = { workflow . id } workflow = { workflow } />
290+ ) ) }
291+ </ div >
292+ </ >
293+ ) ;
294+ }
295+
296+ if ( error ) {
297+ return < ErrorComponent error = { error } reset = { ( ) => { } } /> ;
93298 }
94299
95300 const onDrop = async ( files : ChangeEvent < HTMLInputElement > ) => {
@@ -147,13 +352,19 @@ export default function WorkflowsPage() {
147352 < >
148353 < main className = "flex flex-col gap-8" >
149354 < div className = "flex flex-col gap-6" >
150- < div className = "flex justify-between items-center " >
355+ < div className = "flex justify-between items-end " >
151356 < div >
152- < PageTitle > Workflows</ PageTitle >
357+ < PageTitle > My Workflows</ PageTitle >
153358 < PageSubtitle >
154359 Automate your alert management with workflows
155360 </ PageSubtitle >
156361 </ div >
362+ < SearchInput
363+ className = "flex-1 mx-4"
364+ placeholder = "Search workflows"
365+ value = { searchedValue as string }
366+ onValueChange = { setSearchedValue }
367+ />
157368 < div className = "flex gap-2" >
158369 < Button
159370 color = "orange"
@@ -178,13 +389,44 @@ export default function WorkflowsPage() {
178389 </ Button >
179390 </ div >
180391 </ div >
181- { workflows . length === 0 ? (
392+ { isEmptyState ? (
182393 < WorkflowsEmptyState />
183394 ) : (
184- < div className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 w-full gap-4" >
185- { workflows . map ( ( workflow ) => (
186- < WorkflowTile key = { workflow . id } workflow = { workflow } />
187- ) ) }
395+ < div className = "flex gap-4" >
396+ < FacetsPanelServerSide
397+ entityName = { "workflows" }
398+ facetsConfig = { facetsConfig }
399+ facetOptionsCel = { searchCel }
400+ usePropertyPathsSuggestions = { true }
401+ clearFiltersToken = { clearFiltersToken }
402+ initialFacetsData = { initialFacetsData }
403+ onCelChange = { ( cel ) => setFilterCel ( cel ) }
404+ />
405+
406+ < div className = "flex flex-col flex-1 relative" >
407+ { isFirstLoading && (
408+ < div className = "flex items-center justify-center h-96 w-full" >
409+ < KeepLoader includeMinHeight = { false } />
410+ </ div >
411+ ) }
412+ { ! isFirstLoading && (
413+ < >
414+ { showFilterEmptyState && renderFilterEmptyState ( ) }
415+ { showSearchEmptyState && renderSearchEmptyState ( ) }
416+ { ! isTableEmpty && renderData ( ) }
417+ </ >
418+ ) }
419+ < div className = { `mt-4 ${ isFirstLoading ? "hidden" : "" } ` } >
420+ < Pagination
421+ totalCount = { filteredWorkflowsCount }
422+ isRefreshAllowed = { false }
423+ isRefreshing = { false }
424+ pageSizeOptions = { [ 12 , 24 , 48 ] }
425+ onRefresh = { ( ) => { } }
426+ onStateChange = { setPaginationStateCallback }
427+ />
428+ </ div >
429+ </ div >
188430 </ div >
189431 ) }
190432 </ div >
0 commit comments