Skip to content

Commit

Permalink
Merge pull request #522 from openchatai/ui/better-actions-ui
Browse files Browse the repository at this point in the history
Better UI for users to manage their actions without creating flows
  • Loading branch information
faltawy authored Jan 15, 2024
2 parents 5440679 + bf14917 commit ae0ced2
Show file tree
Hide file tree
Showing 22 changed files with 462 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function ListWorkflows({ params }: Props) {
<HeaderShell className='justify-between'>
<div className='flex items-center gap-2'>
<Workflow size={24} />
<h1 className='text-lg font-semibold'>Flows & Actions</h1>
<h1 className='text-lg font-semibold'>Flows</h1>
</div>
<div>
<CreateWorkflowForm />
Expand Down
109 changes: 109 additions & 0 deletions dashboard/app/(copilot)/copilot/[copilot_id]/actions/Action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { Settings2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { methodVariants } from "@/components/domain/MethodRenderer";
import { useRef } from "react";
import { Row } from "@tanstack/react-table";
import { Stack } from "@/components/ui/Stack";
import { ActionForm } from "@/components/domain/action-form/ActionForm";
import { ActionWithModifiedParametersResponse } from "@/data/actions";
import { useUpdateAction } from "@/hooks/useActions";
import { useCopilot } from "../../_context/CopilotProvider";

export function Action({ getValue, original }: Row<ActionWithModifiedParametersResponse>) {
const {
id: copilotId
} = useCopilot();
const containerRef = useRef<HTMLDivElement>(null);

const [
state,
updateAction
] = useUpdateAction(copilotId, original.id)

return <Stack
ref={containerRef}
ic="start"
gap={5}
direction="column"
className='bg-secondary p-3.5 rounded-lg transition-all col-span-full lg:col-span-6 xl:col-span-4 border border-primary/20 shadow-sm'>
<Stack ic="start" gap={10} id="some" direction="row">
<div className='text-sm font-semibold flex-1'>{getValue("name")}</div>
{/* @ts-ignore */}
<div className={methodVariants({ size: "tiny", method: String(getValue("request_type")).toUpperCase() })}>{getValue("request_type")}</div>
</Stack>
<div className='text-xs text-gray-500'>{getValue("description")}</div>
<Stack
direction="row"
js="between">
<div>
<span className="text-secondary-foreground font-medium text-xs text-start whitespace-nowrap">
Created {" "}{getValue("created_at")}
</span>
</div>
<Stack className="mt-auto gap-1" js="end">
<AlertDialog>
<AlertDialogContent className="overflow-hidden">
</AlertDialogContent>
<AlertDialogTrigger asChild>
<Button size='fit'>
<Settings2 size={15} />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Edit Action</AlertDialogTitle>
</AlertDialogHeader>
{/* */}
<ActionForm defaultValues={original}
className="overflow-auto no-scrollbar"
onSubmit={async (data) => {
await updateAction(data)
}}
footer={() => <AlertDialogFooter className="my-4">
<AlertDialogCancel asChild>
<Button variant='secondary'>
Cancel
</Button>
</AlertDialogCancel>
<Button variant='default' type="submit" loading={state.loading}>
Save
</Button>
</AlertDialogFooter>
}
/>
{/* */}

</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Action</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this action? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel asChild>
<Button variant='secondary' size='fit'>
Cancel
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button variant='destructive' size='fit'>
Delete
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
<AlertDialogTrigger asChild>
<Button variant='destructiveOutline' size='fit'>
<Trash2 size={15} />
</Button>
</AlertDialogTrigger>
</AlertDialog>
</Stack>
</Stack>
</Stack>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use client';
import { Input } from "@/components/ui/input";
import { useListActions } from "@/hooks/useActions";
import { ColumnDef, ColumnFiltersState, RowSelectionState, SortingState, getCoreRowModel, getFilteredRowModel, getGroupedRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
import { useCallback, useState } from "react";
import { Stack } from "@/components/ui/Stack";
import { format } from "timeago.js";
import _ from "lodash";
import { EmptyBlock } from "@/components/domain/EmptyBlock";
import { DataTablePagination } from "@/components/ui/TablePagination";
import { Action } from "./Action";
import { ActionWithModifiedParametersResponse } from "@/data/actions";

const columns: ColumnDef<ActionWithModifiedParametersResponse>[] = [
{
accessorKey: "id",
id: "id"
},
{
accessorKey: "name",
id: "name"
}, {
accessorKey: "description",
id: "description"
}, {
accessorKey: "request_type",
id: "request_type"
},
{
id: "created_at",
accessorFn: (value) => format(value.created_at)
},
{
id: "updated_at",
accessorFn: (value) => format(value.updated_at)
}
];

export function ActionsDataGrid({ copilot_id }: { copilot_id: string }) {
const { data: actions } = useListActions(copilot_id);
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const grid = useReactTable({
data: actions || [],
columns,
pageCount: 1,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getGroupedRowModel: getGroupedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
enableFilters: true,
enableColumnFilters: true,
enableSorting: true,
enableMultiRowSelection: false,
state: {
sorting,
columnFilters,
rowSelection,
}
})

const filterByName = useCallback((name: string) => {
grid.getColumn("name")?.setFilterValue(name)
}, [
grid
])
console.log(
grid.getSelectedRowModel().rows
)
return (
<div className="container">
<Stack fluid direction="row" js="between" className="!gap-5">
<Input placeholder="Search"
className="flex-1"
onChange={(e) => filterByName(e.target.value)}
/>
</Stack>
<div className="grid gap-4 grid-cols-12 mt-5">
{_.isEmpty(grid.getRowModel().rows) ?
<EmptyBlock>
<h1 className="text-lg font-semibold">No actions found</h1>
<p className="text-sm text-gray-500">Create a new action to get started</p>
</EmptyBlock>
: grid.getRowModel().rows.map((item, index) => (
<Action key={index} {...item} />
))}
</div>
<div hidden={grid.getPageCount() === 1} className="mt-4">
<DataTablePagination table={grid} />
</div>
</div>
)
}
44 changes: 44 additions & 0 deletions dashboard/app/(copilot)/copilot/[copilot_id]/actions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';
import { HeaderShell } from '@/components/domain/HeaderShell'
import { RouteIcon } from 'lucide-react';
import { ActionsDataGrid } from './ActionsDataGrid';
import { Suspense } from 'react';
import { Stack } from '@/components/ui/Stack';
import { Button } from '@/components/ui/button';
import { AddActionDrawer, useActionFormState } from '@/components/domain/new-flows-editor/addActionDrawer';

type Props = {
params: {
copilot_id: string;
}
}

export default function ActionsPage({ params }: Props) {
const [
,
setActionFormState
] = useActionFormState();
return (
<Stack
ic='center'
direction='column'
fluid
className='size-full'>
<HeaderShell className='justify-between'>
<div className='flex items-center gap-2'>
<RouteIcon size={24} />
<h1 className='text-lg font-semibold'>Actions</h1>
</div>
<div>
<Button onClick={() => setActionFormState(true)} variant='default'>Create</Button>
</div>
</HeaderShell>
<AddActionDrawer />
<div className='flex-1 w-full p-4 overflow-auto'>
<Suspense fallback={<div>Loading...</div>}>
<ActionsDataGrid copilot_id={params.copilot_id} />
</Suspense>
</div>
</Stack>
)
}
7 changes: 6 additions & 1 deletion dashboard/app/(copilot)/copilot/[copilot_id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
MessagesSquare,
Settings,
Workflow,
SquareCode, Codesandbox
SquareCode, Codesandbox, Route
} from "lucide-react";
import React from "react";
import { CopilotLayoutNavLink } from "../../_parts/CopilotNavLink";
Expand Down Expand Up @@ -74,6 +74,11 @@ export default function CopilotLayout({ children, params }: Props) {
IconComponent={Settings}
label="Settings"
/>
<CopilotLayoutNavLink
href={copilotBase + "/actions"}
IconComponent={Route}
label="Actions"
/>
</div>
</div>
<div className="mx-auto pb-5 flex flex-col items-center gap-2">
Expand Down
9 changes: 8 additions & 1 deletion dashboard/components/domain/MethodRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cva } from "class-variance-authority";


export const methodVariants = cva('text-accent uppercase rounded-md px-2 py-1.5 font-semibold text-xs', {
export const methodVariants = cva('text-accent uppercase text-xs font-semibold', {
variants: {
method: {
GET: 'bg-green-500',
Expand All @@ -13,6 +13,13 @@ export const methodVariants = cva('text-accent uppercase rounded-md px-2 py-1.5
HEAD: 'bg-gray-500',
TRACE: 'bg-gray-500',
CONNECT: 'bg-gray-500',
},
size: {
tiny: 'px-1 py-0.5 rounded-sm',
xs: 'px-2 py-1.5 rounded-md',
}
}, defaultVariants: {
size: 'xs',
method: 'GET'
}
})
22 changes: 16 additions & 6 deletions dashboard/components/domain/action-form/ActionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,19 @@ function transformAction(action: ActionType): ActionWithModifiedParameters {
})
return _.merge(_.omit(action, ['parameters', 'headers']), { payload: { parameters, headers: action.headers } })
}
function actionToForm(action: ActionWithModifiedParameters): ActionType {
const parameters: ActionType['parameters'] = [];

action.payload.parameters?.forEach((parameter) => {
const value = parameter.value ?? undefined;
parameters.push({
key: parameter.name,
value: value,
is_magic: parameter.value === null
})
})
return _.merge(_.omit(action, ['payload']), { parameters, headers: action.payload.headers })
}
function isValidField<
TValues extends FieldValues = FieldValues,
TName extends FieldPath<TValues> = FieldPath<TValues>
Expand All @@ -65,17 +77,17 @@ export function ActionForm({
footer,
className
}: {
defaultValues?: ActionType,
defaultValues?: ActionWithModifiedParameters,
// eslint-disable-next-line no-unused-vars
onSubmit?: (data: ActionWithModifiedParameters, defaultValues?: ActionType) => void
onSubmit?: (data: ActionWithModifiedParameters, defaultValues?: ActionWithModifiedParameters) => void
// eslint-disable-next-line no-unused-vars
footer?: (form: ReturnType<typeof useForm<ActionType>>) => React.ReactNode
className?: string
}) {

const form = useForm<ActionType>({
resolver: zodResolver(actionSchema),
defaultValues: defaultValues ?? {
defaultValues: defaultValues ? actionToForm(defaultValues) : {
request_type: 'GET',
}
});
Expand Down Expand Up @@ -168,7 +180,6 @@ export function ActionForm({
fields.map((field, index) => {
const isValid = isValidField(form.formState, `headers.${index}.value`)
const is_magic = form.watch(`headers.${index}.is_magic`)
const magic_field = `headers.${index}.is_magic`
return (
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1" key={field.id}>
<input
Expand All @@ -190,8 +201,7 @@ export function ActionForm({
</p>}>
<Button
onClick={() => {
// @ts-ignore
form.setValue(magic_field, !is_magic)
form.setValue(`headers.${index}.is_magic`, !is_magic)
}}
data-value={is_magic}
size='fit'
Expand Down
4 changes: 2 additions & 2 deletions dashboard/components/domain/new-flows-editor/ActionBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { Handle, Position, useNodes } from 'reactflow';
import type { NodeProps } from 'reactflow';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import { ActionResponseType } from '@/data/actions';
import { ActionWithModifiedParametersResponse } from '@/data/actions';
import { Action } from './ActionsList';
import _, { uniqueId } from 'lodash';
import { SelectedActionPosition, useController } from './Controller';
Expand All @@ -19,7 +19,7 @@ export const BLOCK_ACTION_DRAGGABLE_ID_PREFIX = 'block-action-draggable|';

type Props = NodeProps<BlockType>

function DraggableActionInsideActionBlock({ action, index, id, position }: { action: ActionResponseType, index: number, id: string, position: SelectedActionPosition }) {
function DraggableActionInsideActionBlock({ action, index, id, position }: { action: ActionWithModifiedParametersResponse, index: number, id: string, position: SelectedActionPosition }) {
const draggableInPortal = useDraggableInPortal();
const { selectedActions } = useController();
const isSelected = selectedActions.isSelected(position);
Expand Down
Loading

0 comments on commit ae0ced2

Please sign in to comment.