From 31642ffe1d9e8a94bdc682ed6323dd6f24ca99e7 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:12:14 -0700 Subject: [PATCH 01/12] Initialize frontend search and filter. --- app/page.tsx | 218 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 158 insertions(+), 60 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index a4dac71..fde672f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo, Fragment } from "react"; import Link from "next/link"; -import { Button, Flex, Spin, Table } from "antd"; -import type { Breakpoint } from "antd"; +import { Button, Flex, Spin, Table, Input, Tag } from "antd"; +import type { Breakpoint, TablePaginationConfig, TableColumnsType } from "antd"; +import type { FilterValue, SorterResult } from "antd/es/table/interface"; import { EyeOutlined, EditOutlined, CheckCircleOutlined, DownSquareOutlined } from "@ant-design/icons"; import { RuleInfo } from "./types/ruleInfo"; import { getAllRuleData } from "./utils/api"; @@ -11,6 +12,12 @@ import styles from "./styles/home.module.css"; export default function Home() { const [rules, setRules] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [filteredInfo, setFilteredInfo] = useState>({}); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 15, + }); useEffect(() => { const getRules = async () => { @@ -25,75 +32,155 @@ export default function Home() { getRules(); }, []); - const mappedRules = rules.map(({ _id, title, filepath, reviewBranch, isPublished }) => { - const ruleLink = `/rule/${_id}`; - const draftLink = `${ruleLink}?version=draft`; - return { - key: _id, - titleLink: ( - - - {title || filepath} - - - ), - versions: ( - - - {reviewBranch && ( + const handleSearch = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); + setPagination((prev) => ({ ...prev, current: 1 })); + }; + + const formatFilePathTags = (filepath: string) => { + const parts = filepath.split("/"); + return parts.map((part, index) => ( + {index < parts.length - 1 && {part}} + )); + }; + + // Generate filters for filepaths based on the full filepath of each rule + const getFilepathFilters = useMemo(() => { + const directories = new Set(); + + rules.forEach((rule) => { + const parts = rule.filepath.split("/"); + let currentPath = ""; + + parts.forEach((part, index) => { + if (index < parts.length - 1) { + currentPath += (currentPath ? "/" : "") + part; + directories.add(currentPath); + } + }); + }); + + return Array.from(directories).map((path) => ({ + text: path.split("/").pop() || "", + value: path, + })); + }, [rules]); + + // Filter rules based on search terms and filepath filters + const filteredRules = useMemo(() => { + let result = rules; + if (searchTerm) { + result = result.filter( + (rule) => + rule?.title?.toLowerCase().includes(searchTerm.toLowerCase()) || + rule.filepath.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + if (filteredInfo.filepath) { + const filepathFilters = filteredInfo.filepath as string[]; + result = result.filter((rule) => filepathFilters.some((path) => rule.filepath.startsWith(path))); + } + + return result; + }, [rules, searchTerm, filteredInfo]); + + const columns: TableColumnsType = [ + { + title: "Rule", + dataIndex: "title", + key: "title", + sorter: (a, b) => (a.title || "").localeCompare(b.title || ""), + render: (_, record) => { + const ruleLink = `/rule/${record._id}`; + const draftLink = `${ruleLink}?version=draft`; + return ( + + + {record.title || record.filepath} + + + ); + }, + }, + { + title: "Categories", + dataIndex: "filepath", + key: "filepath", + filters: getFilepathFilters, + filteredValue: filteredInfo.filepath || null, + onFilter: (value, record) => record.filepath.startsWith(value as string), + filterMode: "tree", + filterSearch: true, + sorter: (a, b) => a.filepath.localeCompare(b.filepath), + render: (_, record) => {formatFilePathTags(record.filepath)}, + }, + { + title: "Versions", + key: "versions", + responsive: ["md" as Breakpoint], + render: (_, record) => { + const ruleLink = `/rule/${record._id}`; + const draftLink = `${ruleLink}?version=draft`; + return ( + - )} - {isPublished && ( - <> - + {record.reviewBranch && ( - - )} - - ), - }; - }); - - const columns = [ - { - dataIndex: "titleLink", + )} + {record.isPublished && ( + <> + + + + )} + + ); + }, }, - { dataIndex: "versions", responsive: ["md" as Breakpoint] }, ]; + const handleTableChange = ( + newPagination: TablePaginationConfig, + filters: Record, + sorter: SorterResult | SorterResult[] + ) => { + setPagination(newPagination); + setFilteredInfo(filters); + }; + return ( <> @@ -107,12 +194,23 @@ export default function Home() { + {isLoading ? (
) : ( - +
)} ); From 8e27ddfb305f671e7247f04272996ac6e5fc5d85 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:49:32 -0700 Subject: [PATCH 02/12] Init server-side search and filter. --- app/page.tsx | 166 ++++++++++++++++++++------------------- app/types/ruleInfo.d.ts | 6 ++ app/types/rulequery.d.ts | 10 +++ app/utils/api.ts | 7 +- 4 files changed, 104 insertions(+), 85 deletions(-) create mode 100644 app/types/rulequery.d.ts diff --git a/app/page.tsx b/app/page.tsx index fde672f..ad3c51b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,95 +1,94 @@ "use client"; -import { useState, useEffect, useMemo, Fragment } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import Link from "next/link"; import { Button, Flex, Spin, Table, Input, Tag } from "antd"; import type { Breakpoint, TablePaginationConfig, TableColumnsType } from "antd"; -import type { FilterValue, SorterResult } from "antd/es/table/interface"; +import type { ColumnFilterItem, FilterValue, SorterResult } from "antd/es/table/interface"; import { EyeOutlined, EditOutlined, CheckCircleOutlined, DownSquareOutlined } from "@ant-design/icons"; import { RuleInfo } from "./types/ruleInfo"; import { getAllRuleData } from "./utils/api"; import styles from "./styles/home.module.css"; +interface TableParams { + pagination?: TablePaginationConfig; + sortField?: string; + sortOrder?: string; + filters?: Record; + searchTerm?: string; +} + export default function Home() { const [rules, setRules] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [searchTerm, setSearchTerm] = useState(""); - const [filteredInfo, setFilteredInfo] = useState>({}); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 15, + const [loading, setLoading] = useState(true); + const [categories, setCategories] = useState([]); + const [tableParams, setTableParams] = useState({ + pagination: { + current: 1, + pageSize: 15, + total: 0, + }, + filters: {}, + searchTerm: "", }); - useEffect(() => { - const getRules = async () => { - try { - const ruleData = await getAllRuleData(); - setRules(ruleData); - setIsLoading(false); - } catch (error) { - console.error(`Error loading rules: ${error}`); - } - }; - getRules(); - }, []); + const fetchData = async () => { + setLoading(true); + try { + // Update this to match your API structure + const response = await getAllRuleData({ + page: tableParams.pagination?.current || 1, + pageSize: tableParams.pagination?.pageSize, + sortField: tableParams.sortField, + sortOrder: tableParams.sortOrder, + filters: tableParams.filters, + searchTerm: tableParams.searchTerm, + }); + + setRules(response.data); + setCategories(response.categories.map((category: string) => ({ text: category, value: category }))); + setTableParams({ + ...tableParams, + pagination: { + ...tableParams.pagination, + total: response.total, // Assuming your API returns the total count + }, + }); + } catch (error) { + console.error(`Error loading rules: ${error}`); + } finally { + setLoading(false); + } + }; + + useEffect( + () => { + fetchData(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(tableParams)] + ); - const handleSearch = (e: React.ChangeEvent) => { - setSearchTerm(e.target.value); - setPagination((prev) => ({ ...prev, current: 1 })); + const handleSearch = (value: string) => { + setTableParams({ + ...tableParams, + searchTerm: value, + pagination: { ...tableParams.pagination, current: 1 }, + }); }; const formatFilePathTags = (filepath: string) => { const parts = filepath.split("/"); return parts.map((part, index) => ( - {index < parts.length - 1 && {part}} + {index < parts.length - 1 && {part}} )); }; - // Generate filters for filepaths based on the full filepath of each rule - const getFilepathFilters = useMemo(() => { - const directories = new Set(); - - rules.forEach((rule) => { - const parts = rule.filepath.split("/"); - let currentPath = ""; - - parts.forEach((part, index) => { - if (index < parts.length - 1) { - currentPath += (currentPath ? "/" : "") + part; - directories.add(currentPath); - } - }); - }); - - return Array.from(directories).map((path) => ({ - text: path.split("/").pop() || "", - value: path, - })); - }, [rules]); - - // Filter rules based on search terms and filepath filters - const filteredRules = useMemo(() => { - let result = rules; - if (searchTerm) { - result = result.filter( - (rule) => - rule?.title?.toLowerCase().includes(searchTerm.toLowerCase()) || - rule.filepath.toLowerCase().includes(searchTerm.toLowerCase()) - ); - } - if (filteredInfo.filepath) { - const filepathFilters = filteredInfo.filepath as string[]; - result = result.filter((rule) => filepathFilters.some((path) => rule.filepath.startsWith(path))); - } - - return result; - }, [rules, searchTerm, filteredInfo]); - const columns: TableColumnsType = [ { title: "Rule", dataIndex: "title", key: "title", - sorter: (a, b) => (a.title || "").localeCompare(b.title || ""), + sorter: true, render: (_, record) => { const ruleLink = `/rule/${record._id}`; const draftLink = `${ruleLink}?version=draft`; @@ -106,12 +105,9 @@ export default function Home() { title: "Categories", dataIndex: "filepath", key: "filepath", - filters: getFilepathFilters, - filteredValue: filteredInfo.filepath || null, - onFilter: (value, record) => record.filepath.startsWith(value as string), - filterMode: "tree", - filterSearch: true, - sorter: (a, b) => a.filepath.localeCompare(b.filepath), + filters: categories, + filteredValue: tableParams.filters?.filepath || null, + sorter: true, render: (_, record) => {formatFilePathTags(record.filepath)}, }, { @@ -173,12 +169,21 @@ export default function Home() { ]; const handleTableChange = ( - newPagination: TablePaginationConfig, + pagination: TablePaginationConfig, filters: Record, sorter: SorterResult | SorterResult[] ) => { - setPagination(newPagination); - setFilteredInfo(filters); + console.log(pagination, "this is pagination"); + console.log(filters, "this is filters"); + console.log(sorter, "this is sorter"); + setTableParams({ + pagination, + filters, + ...(!Array.isArray(sorter) && { + sortField: sorter.field as string, + sortOrder: sorter.order ? sorter.order : undefined, + }), + }); }; return ( @@ -194,20 +199,17 @@ export default function Home() { - - {isLoading ? ( + + {loading ? (
) : (
diff --git a/app/types/ruleInfo.d.ts b/app/types/ruleInfo.d.ts index 3ca19b5..31cee70 100644 --- a/app/types/ruleInfo.d.ts +++ b/app/types/ruleInfo.d.ts @@ -15,3 +15,9 @@ export interface RuleInfo extends RuleInfoBasic { reviewBranch?: string; isPublished?: boolean; } + +export interface RuleDataResponse { + data: RuleInfo[]; + total: number; + categories: Array; +} diff --git a/app/types/rulequery.d.ts b/app/types/rulequery.d.ts new file mode 100644 index 0000000..3cc5d27 --- /dev/null +++ b/app/types/rulequery.d.ts @@ -0,0 +1,10 @@ +import { FilterValue } from "antd/es/table/interface"; + +export interface RuleQuery { + page?: number; + pageSize?: number; + sortField?: string; + sortOrder?: string; + filters?: Record | undefined; + searchTerm?: string; +} diff --git a/app/utils/api.ts b/app/utils/api.ts index 327289e..2570b2e 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -1,10 +1,11 @@ import { DecisionGraphType } from "@gorules/jdm-editor"; import axios from "axios"; -import { RuleDraft, RuleInfo } from "../types/ruleInfo"; +import { RuleDataResponse, RuleDraft, RuleInfo } from "../types/ruleInfo"; import { RuleMap } from "../types/rulemap"; import { KlammBREField } from "../types/klamm"; import { downloadFileBlob, generateDescriptiveName, getShortFilenameOnly } from "./utils"; import { valueType } from "antd/es/statistic/utils"; +import { RuleQuery } from "../types/rulequery"; const axiosAPIInstance = axios.create({ // For server side calls, need full URL, otherwise can just use /api @@ -51,9 +52,9 @@ export const getRuleDraft = async (ruleId: string): Promise => { * @returns The rule data list. * @throws If an error occurs while fetching the rule data. */ -export const getAllRuleData = async (): Promise => { +export const getAllRuleData = async (params: RuleQuery): Promise => { try { - const { data } = await axiosAPIInstance.get("/ruleData/list"); + const { data } = await axiosAPIInstance.get("/ruleData/list", { params }); return data; } catch (error) { console.error(`Error fetching rule data: ${error}`); From 49a55e9ea775412da71f86eaa921acb6eacb7e3e Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:20:45 -0700 Subject: [PATCH 03/12] Add server-side search and update descriptive components. --- app/admin/page.tsx | 83 +++++++++++++++---- .../InputOutputTable/InputOutputTable.tsx | 6 +- .../InputStyler/subcomponents/FieldStyler.tsx | 24 ++++++ .../subcomponents/InputComponents.tsx | 15 +++- app/components/RuleManager/RuleManager.tsx | 2 +- .../subcomponents/LinkRuleComponent.tsx | 2 +- .../ScenarioFormatter/ScenarioFormatter.tsx | 10 ++- app/page.tsx | 28 +++++-- app/types/ruleInfo.d.ts | 7 +- app/utils/api.ts | 8 +- 10 files changed, 148 insertions(+), 37 deletions(-) create mode 100644 app/components/InputStyler/subcomponents/FieldStyler.tsx diff --git a/app/admin/page.tsx b/app/admin/page.tsx index baed6fb..2f6a3a2 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -2,7 +2,8 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { Table, Input, Button, Flex } from "antd"; -import { ColumnsType } from "antd/es/table"; +import { ColumnsType, TablePaginationConfig } from "antd/es/table"; +import { FilterValue } from "antd/es/table/interface"; import { HomeOutlined } from "@ant-design/icons"; import { RuleInfo, RuleInfoBasic } from "../types/ruleInfo"; import { getAllRuleData, postRuleData, updateRuleData, deleteRuleData } from "../utils/api"; @@ -15,23 +16,56 @@ enum ACTION_STATUS { const PAGE_SIZE = 15; +interface TableParams { + pagination?: TablePaginationConfig; + searchTerm?: string; +} + export default function Admin() { const [isLoading, setIsLoading] = useState(true); const [initialRules, setInitialRules] = useState([]); const [rules, setRules] = useState([]); - const [currentPage, setCurrentPage] = useState(0); + const [tableParams, setTableParams] = useState({ + pagination: { + current: 1, + pageSize: 15, + total: 0, + }, + searchTerm: "", + }); const getOrRefreshRuleList = async () => { - // Get rules that are already defined in the DB - const existingRules = await getAllRuleData(); - setInitialRules(existingRules); - setRules(JSON.parse(JSON.stringify([...existingRules]))); // JSON.parse(JSON.stringify(data)) is a hacky way to deep copy the data - needed for comparison later - setIsLoading(false); + setIsLoading(true); + try { + const ruleData = await getAllRuleData({ + page: tableParams.pagination?.current || 1, + pageSize: tableParams.pagination?.pageSize || 15, + searchTerm: tableParams.searchTerm || "", + }); + const existingRules = ruleData?.data || []; + setInitialRules(existingRules); + setRules(JSON.parse(JSON.stringify([...existingRules]))); + setTableParams({ + ...tableParams, + pagination: { + ...tableParams.pagination, + total: ruleData?.total || 0, + }, + }); + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } }; - useEffect(() => { - getOrRefreshRuleList(); - }, []); + useEffect( + () => { + getOrRefreshRuleList(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(tableParams)] + ); const updateRule = (e: React.ChangeEvent, index: number, property: keyof RuleInfoBasic) => { const newRules = [...rules]; @@ -40,7 +74,8 @@ export default function Admin() { }; const deleteRule = async (index: number) => { - const deletionIndex = (currentPage - 1) * PAGE_SIZE + index; + const deletionIndex = + ((tableParams?.pagination?.current || 1) - 1) * (tableParams?.pagination?.pageSize || PAGE_SIZE) + index; const newRules = [...rules.slice(0, deletionIndex), ...rules.slice(deletionIndex + 1, rules.length)]; setRules(newRules); }; @@ -67,11 +102,6 @@ export default function Admin() { return [...updatedEntries, ...deletedEntries]; }; - const updateCurrPage = (page: number, pageSize: number) => { - // Keep track of current page so we can delete via splice properly - setCurrentPage(page); - }; - // Save all rule updates to the API/DB const saveAllRuleUpdates = async () => { setIsLoading(true); @@ -136,6 +166,21 @@ export default function Admin() { }, ]; + const handleTableChange = (pagination: TablePaginationConfig, filters: Record) => { + setTableParams((prevParams) => ({ + pagination, + filters, + searchTerm: prevParams.searchTerm, + })); + }; + const handleSearch = (value: string) => { + setTableParams({ + ...tableParams, + searchTerm: value, + pagination: { ...tableParams.pagination, current: 1 }, + }); + }; + return ( <> @@ -149,12 +194,16 @@ export default function Admin() { )} + + {isLoading ? (

Loading...

) : (
({ key, ...rule }))} /> )} diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 65bb8a3..a450f4b 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -4,6 +4,7 @@ import { MinusCircleOutlined } from "@ant-design/icons"; import { RuleMap } from "@/app/types/rulemap"; import styles from "./InputOutputTable.module.css"; import { dollarFormat } from "@/app/utils/utils"; +import FieldStyler from "../InputStyler/subcomponents/FieldStyler"; const COLUMNS = [ { @@ -142,7 +143,10 @@ export default function InputOutputTable({ .filter(([field]) => !PROPERTIES_TO_IGNORE.includes(field)) .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) .map(([field, value], index) => ({ - field: propertyRuleMap?.find((item) => item.field === field)?.name || field, + field: FieldStyler( + propertyRuleMap?.find((item) => item.field === field)?.name || field, + propertyRuleMap?.find((item) => item.field === field)?.description + ), value: convertAndStyleValue(value, field, editable), key: index, })); diff --git a/app/components/InputStyler/subcomponents/FieldStyler.tsx b/app/components/InputStyler/subcomponents/FieldStyler.tsx new file mode 100644 index 0000000..8f3c760 --- /dev/null +++ b/app/components/InputStyler/subcomponents/FieldStyler.tsx @@ -0,0 +1,24 @@ +import { Tooltip, Popover } from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; + +export default function FieldStyler(fieldName: string, description?: string) { + const popOverInformation = ( + + + {" "} + + + + ); + const helpDialog = "View Description"; + return ( + + ); +} diff --git a/app/components/InputStyler/subcomponents/InputComponents.tsx b/app/components/InputStyler/subcomponents/InputComponents.tsx index adba5e0..d4b63b1 100644 --- a/app/components/InputStyler/subcomponents/InputComponents.tsx +++ b/app/components/InputStyler/subcomponents/InputComponents.tsx @@ -238,7 +238,20 @@ export const ReadOnlyBooleanDisplay = ({ show, value }: ReadOnlyProps) => { export const ReadOnlyStringDisplay = ({ show, value }: ReadOnlyProps) => { if (!show) return null; - return value.toString(); + const stringList = value.split(","); + if (stringList.length > 1) { + return ( + <> + {stringList.map((string: string, index: number) => ( + + {string.trim()} + + ))} + + ); + } + + return {value}; }; export const ReadOnlyNumberDisplay = ({ show, value, field }: ReadOnlyNumberDisplayProps) => { diff --git a/app/components/RuleManager/RuleManager.tsx b/app/components/RuleManager/RuleManager.tsx index ac009ff..9d6eb3a 100644 --- a/app/components/RuleManager/RuleManager.tsx +++ b/app/components/RuleManager/RuleManager.tsx @@ -54,7 +54,7 @@ export default function RuleManager({ const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); const { setHasUnsavedChanges } = useLeaveScreenPopup(); const canEditGraph = editing === "draft" || editing === true; - const canEditScenarios = editing === "draft" || editing === "inreview" || editing === true; + const canEditScenarios = editing === "draft" || editing === "inReview" || editing === true; const updateRuleContent = (updatedRuleContent: DecisionGraphType) => { if (ruleContent !== updatedRuleContent) { diff --git a/app/components/RuleViewerEditor/subcomponents/LinkRuleComponent.tsx b/app/components/RuleViewerEditor/subcomponents/LinkRuleComponent.tsx index b78fe87..8f8d61c 100644 --- a/app/components/RuleViewerEditor/subcomponents/LinkRuleComponent.tsx +++ b/app/components/RuleViewerEditor/subcomponents/LinkRuleComponent.tsx @@ -30,7 +30,7 @@ export default function LinkRuleComponent({ specification, id, isSelected, name, const getRuleOptions = async () => { const ruleData = await getAllRuleData(); setRuleOptions( - ruleData.map(({ title, filepath }) => ({ + ruleData?.data.map(({ title, filepath }) => ({ label: title || filepath, value: filepath, })) diff --git a/app/components/ScenariosManager/ScenarioFormatter/ScenarioFormatter.tsx b/app/components/ScenariosManager/ScenarioFormatter/ScenarioFormatter.tsx index 672201b..f60b366 100644 --- a/app/components/ScenariosManager/ScenarioFormatter/ScenarioFormatter.tsx +++ b/app/components/ScenariosManager/ScenarioFormatter/ScenarioFormatter.tsx @@ -5,6 +5,7 @@ import styles from "./ScenarioFormatter.module.css"; import { RuleMap } from "@/app/types/rulemap"; import InputStyler from "../../InputStyler/InputStyler"; import { parseSchemaTemplate } from "../../InputStyler/InputStyler"; +import FieldStyler from "../../InputStyler/subcomponents/FieldStyler"; const COLUMNS = [ { @@ -62,10 +63,13 @@ export default function ScenarioFormatter({ title, rawData, setRawData, scenario .filter(([field]) => !PROPERTIES_TO_IGNORE.includes(field)) .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) .map(([field, value], index) => ({ - field: + field: FieldStyler( propertyRuleMap?.find((item) => item.field === field)?.name || - parseSchemaTemplate(field)?.arrayName || - field, + parseSchemaTemplate(field)?.arrayName || + field, + propertyRuleMap?.find((item) => item.field === field)?.description + ), + value: InputStyler( value, field, diff --git a/app/page.tsx b/app/page.tsx index ad3c51b..552e370 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect } from "react"; import Link from "next/link"; import { Button, Flex, Spin, Table, Input, Tag } from "antd"; import type { Breakpoint, TablePaginationConfig, TableColumnsType } from "antd"; @@ -34,7 +34,6 @@ export default function Home() { const fetchData = async () => { setLoading(true); try { - // Update this to match your API structure const response = await getAllRuleData({ page: tableParams.pagination?.current || 1, pageSize: tableParams.pagination?.pageSize, @@ -45,12 +44,12 @@ export default function Home() { }); setRules(response.data); - setCategories(response.categories.map((category: string) => ({ text: category, value: category }))); + setCategories(response.categories); setTableParams({ ...tableParams, pagination: { ...tableParams.pagination, - total: response.total, // Assuming your API returns the total count + total: response.total, }, }); } catch (error) { @@ -173,16 +172,24 @@ export default function Home() { filters: Record, sorter: SorterResult | SorterResult[] ) => { - console.log(pagination, "this is pagination"); - console.log(filters, "this is filters"); - console.log(sorter, "this is sorter"); - setTableParams({ + setTableParams((prevParams) => ({ pagination, filters, + searchTerm: prevParams.searchTerm, ...(!Array.isArray(sorter) && { sortField: sorter.field as string, sortOrder: sorter.order ? sorter.order : undefined, }), + })); + }; + + const clearAll = () => { + setTableParams({ + pagination: { current: 1, pageSize: 15, total: 0 }, + filters: {}, + searchTerm: "", + sortField: "", + sortOrder: undefined, }); }; @@ -199,7 +206,10 @@ export default function Home() { - + + + + {loading ? (
diff --git a/app/types/ruleInfo.d.ts b/app/types/ruleInfo.d.ts index 31cee70..cb85904 100644 --- a/app/types/ruleInfo.d.ts +++ b/app/types/ruleInfo.d.ts @@ -16,8 +16,13 @@ export interface RuleInfo extends RuleInfoBasic { isPublished?: boolean; } +export interface CategoryObject { + text: string; + value: string; +} + export interface RuleDataResponse { data: RuleInfo[]; total: number; - categories: Array; + categories: Array; } diff --git a/app/utils/api.ts b/app/utils/api.ts index 2570b2e..deaeabf 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -52,7 +52,7 @@ export const getRuleDraft = async (ruleId: string): Promise => { * @returns The rule data list. * @throws If an error occurs while fetching the rule data. */ -export const getAllRuleData = async (params: RuleQuery): Promise => { +export const getAllRuleData = async (params?: RuleQuery): Promise => { try { const { data } = await axiosAPIInstance.get("/ruleData/list", { params }); return data; @@ -302,6 +302,7 @@ export const runDecisionsForScenarios = async (filepath: string, ruleContent?: D /** * Downloads a CSV file containing scenarios for a rule run. * @param filepath The filename for the JSON rule. + * @param ruleVersion The version of the rule to run. * @param ruleContent The rule decision graph to evaluate. * @returns The processed CSV content as a string. * @throws If an error occurs during file upload or processing. @@ -321,7 +322,7 @@ export const getCSVForRuleRun = async ( } ); - const filename = `${(ruleVersion + "_" + filepath).replace(/\.json$/, ".csv")}`; + const filename = `${filepath.replace(/\.json$/, ".csv")}`; downloadFileBlob(response.data, "text/csv", filename); return "CSV downloaded successfully"; @@ -407,8 +408,9 @@ export const getBREFieldFromName = async (fieldName: string): Promise Date: Thu, 10 Oct 2024 15:21:36 -0700 Subject: [PATCH 04/12] Add klamm refresh button for manual input/output information update. --- .../RuleInputOutputFieldsComponent.tsx | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx b/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx index 53bd3dc..441b7a0 100644 --- a/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx +++ b/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from "react"; -import { Button, List, Select, Spin, Tooltip } from "antd"; -import { DeleteOutlined } from "@ant-design/icons"; +import { Button, List, Select, Spin, Tooltip, message } from "antd"; +import { DeleteOutlined, SyncOutlined } from "@ant-design/icons"; import type { DefaultOptionType } from "antd/es/select"; import type { FlattenOptionData } from "rc-select/lib/interface"; import { GraphNode, useDecisionGraphActions, useDecisionGraphState } from "@gorules/jdm-editor"; @@ -190,6 +190,53 @@ export default function RuleInputOutputFieldsComponent({ ); }; + const refreshFromKlamm = async () => { + setIsLoading(true); + try { + const updatedFields = await Promise.all( + inputOutputFields.map(async (field) => { + if (field.field) { + const klammData: KlammBREField = await getBREFieldFromName(field.field); + return { + id: klammData.id, + field: klammData.name, + name: klammData.label, + description: klammData.description, + dataType: klammData?.data_type?.name, + validationCriteria: klammData?.data_validation?.validation_criteria, + validationType: klammData?.data_validation?.bre_validation_type?.value, + childFields: + klammData?.data_type?.name === "object-array" + ? klammData?.child_fields?.map((child) => ({ + id: child.id, + name: child.label, + field: child.name, + description: child.description, + dataType: child?.bre_data_type?.name, + validationCriteria: child?.bre_data_validation?.validation_criteria, + validationType: child?.bre_data_validation?.bre_validation_type?.value, + })) + : [], + }; + } + return field; + }) + ); + + updateNode(id, (draft) => { + draft.content.fields = updatedFields; + return draft; + }); + + message.success("Fields refreshed successfully from Klamm"); + } catch (error) { + console.error("Error refreshing fields from Klamm:", error); + message.error("Failed to refresh fields from Klamm"); + } finally { + setIsLoading(false); + } + }; + return ( Add {fieldsTypeLabel} + , + , ] : [] } From 73d10c6021a7ccdc712d49ccfe3f106b2d32d0e5 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:20:45 -0700 Subject: [PATCH 05/12] Fix title metadata update on navigation. --- app/page.tsx | 1 + app/rule/[ruleId]/page.tsx | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 552e370..62cd826 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -106,6 +106,7 @@ export default function Home() { key: "filepath", filters: categories, filteredValue: tableParams.filters?.filepath || null, + filterSearch: true, sorter: true, render: (_, record) => {formatFilePathTags(record.filepath)}, }, diff --git a/app/rule/[ruleId]/page.tsx b/app/rule/[ruleId]/page.tsx index 1aaf640..5d10aba 100644 --- a/app/rule/[ruleId]/page.tsx +++ b/app/rule/[ruleId]/page.tsx @@ -8,15 +8,24 @@ import { RULE_VERSION } from "@/app/constants/ruleVersion"; import { GithubAuthProvider } from "@/app/components/GithubAuthProvider"; import useGithubAuth from "@/app/hooks/useGithubAuth"; -export let metadata: Metadata; - -export default async function Rule({ - params: { ruleId }, - searchParams, -}: { +type Props = { params: { ruleId: string }; searchParams: { version?: string }; -}) { +}; + +// Update page title with rule name +export async function generateMetadata({ params, searchParams }: Props): Promise { + const { ruleId } = params; + const { version } = searchParams; + + const { ruleInfo } = await getRuleDataForVersion(ruleId, version); + + return { + title: ruleInfo.title, + }; +} + +export default async function Rule({ params: { ruleId }, searchParams }: Props) { // Get version of rule to use const { version } = searchParams; @@ -32,9 +41,6 @@ export default async function Rule({ return

Rule not found

; } - // Update page title with rule name - metadata = { title: ruleInfo.title }; - // Get scenario information const scenarios: Scenario[] = await getScenariosByFilename(ruleInfo.filepath); From 101d76067e80747fb7513115e1563aa063726535 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:37:23 -0700 Subject: [PATCH 06/12] Add loading after selecting generate tests button. --- .../IsolationTester/IsolationTester.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx b/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx index 0d16bb9..db190d3 100644 --- a/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx +++ b/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Flex, Button, message, InputNumber, Collapse } from "antd"; +import { Flex, Button, message, InputNumber, Collapse, Spin } from "antd"; import { Scenario } from "@/app/types/scenario"; import { getCSVTests } from "@/app/utils/api"; import { RuleMap } from "@/app/types/rulemap"; @@ -30,15 +30,19 @@ export default function IsolationTester({ ruleVersion, }: IsolationTesterProps) { const [testScenarioCount, setTestScenarioCount] = useState(10); + const [loading, setLoading] = useState(false); const handleCSVTests = async () => { const ruleName = ruleVersion === "draft" ? "Draft" : ruleVersion === "inreview" ? "In Review" : "Published"; try { + setLoading(true); const csvContent = await getCSVTests(jsonFile, ruleName, ruleContent, simulationContext, testScenarioCount); message.success(`Scenario Tests: ${csvContent}`); } catch (error) { message.error("Error downloading scenarios."); console.error("Error:", error); + } finally { + setLoading(false); } }; @@ -102,9 +106,15 @@ export default function IsolationTester({
  • Generate a CSV file with your created tests:{" "} - + {loading ? ( + +
    + + ) : ( + + )}
  • From c1b19d9394109e3a0d9b0d9edd9239b666efddf4 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:19:56 -0700 Subject: [PATCH 07/12] Style and refactoring updates. --- app/admin/page.tsx | 4 ++-- .../InputOutputTable/InputOutputTable.tsx | 16 ++++++++-------- app/components/RuleManager/RuleManager.tsx | 5 +++-- .../IsolationTester/IsolationTester.tsx | 5 +---- .../ScenariosManager/ScenarioCSV/ScenarioCSV.tsx | 6 ++---- .../ScenariosManager/ScenariosManager.tsx | 3 +-- app/page.tsx | 13 ++++++++++--- app/utils/api.ts | 8 +------- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 2f6a3a2..17ec803 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -74,8 +74,8 @@ export default function Admin() { }; const deleteRule = async (index: number) => { - const deletionIndex = - ((tableParams?.pagination?.current || 1) - 1) * (tableParams?.pagination?.pageSize || PAGE_SIZE) + index; + const { current, pageSize } = tableParams?.pagination || {}; + const deletionIndex = ((current || 1) - 1) * (pageSize || PAGE_SIZE) + index; const newRules = [...rules.slice(0, deletionIndex), ...rules.slice(deletionIndex + 1, rules.length)]; setRules(newRules); }; diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index a450f4b..2c5d2c2 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -142,14 +142,14 @@ export default function InputOutputTable({ const newData = Object.entries(rawData) .filter(([field]) => !PROPERTIES_TO_IGNORE.includes(field)) .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) - .map(([field, value], index) => ({ - field: FieldStyler( - propertyRuleMap?.find((item) => item.field === field)?.name || field, - propertyRuleMap?.find((item) => item.field === field)?.description - ), - value: convertAndStyleValue(value, field, editable), - key: index, - })); + .map(([field, value], index) => { + const propertyRule = propertyRuleMap?.find((item) => item.field === field); + return { + field: FieldStyler(propertyRule?.name || field, propertyRule?.description), + value: convertAndStyleValue(value, field, editable), + key: index, + }; + }); setDataSource(newData); const newColumns = COLUMNS.filter((column) => showColumn(newData, column.dataIndex)); setColumns(newColumns); diff --git a/app/components/RuleManager/RuleManager.tsx b/app/components/RuleManager/RuleManager.tsx index 9d6eb3a..cf21fdc 100644 --- a/app/components/RuleManager/RuleManager.tsx +++ b/app/components/RuleManager/RuleManager.tsx @@ -9,6 +9,7 @@ import { RuleMap } from "@/app/types/rulemap"; import { Scenario } from "@/app/types/scenario"; import useLeaveScreenPopup from "@/app/hooks/useLeaveScreenPopup"; import { DEFAULT_RULE_CONTENT } from "@/app/constants/defaultRuleContent"; +import { RULE_VERSION } from "@/app/constants/ruleVersion"; import SavePublish from "../SavePublish"; import ScenariosManager from "../ScenariosManager"; import styles from "./RuleManager.module.css"; @@ -53,8 +54,8 @@ export default function RuleManager({ const [simulationContext, setSimulationContext] = useState>(); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); const { setHasUnsavedChanges } = useLeaveScreenPopup(); - const canEditGraph = editing === "draft" || editing === true; - const canEditScenarios = editing === "draft" || editing === "inReview" || editing === true; + const canEditGraph = editing === RULE_VERSION.draft || editing === true; + const canEditScenarios = editing === RULE_VERSION.draft || editing === RULE_VERSION.inReview || editing === true; const updateRuleContent = (updatedRuleContent: DecisionGraphType) => { if (ruleContent !== updatedRuleContent) { diff --git a/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx b/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx index db190d3..4b638e3 100644 --- a/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx +++ b/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx @@ -16,7 +16,6 @@ interface IsolationTesterProps { jsonFile: string; rulemap: RuleMap; ruleContent?: DecisionGraphType; - ruleVersion?: string | boolean; } export default function IsolationTester({ @@ -27,16 +26,14 @@ export default function IsolationTester({ jsonFile, rulemap, ruleContent, - ruleVersion, }: IsolationTesterProps) { const [testScenarioCount, setTestScenarioCount] = useState(10); const [loading, setLoading] = useState(false); const handleCSVTests = async () => { - const ruleName = ruleVersion === "draft" ? "Draft" : ruleVersion === "inreview" ? "In Review" : "Published"; try { setLoading(true); - const csvContent = await getCSVTests(jsonFile, ruleName, ruleContent, simulationContext, testScenarioCount); + const csvContent = await getCSVTests(jsonFile, ruleContent, simulationContext, testScenarioCount); message.success(`Scenario Tests: ${csvContent}`); } catch (error) { message.error("Error downloading scenarios."); diff --git a/app/components/ScenariosManager/ScenarioCSV/ScenarioCSV.tsx b/app/components/ScenariosManager/ScenarioCSV/ScenarioCSV.tsx index f5570c6..bcc219d 100644 --- a/app/components/ScenariosManager/ScenarioCSV/ScenarioCSV.tsx +++ b/app/components/ScenariosManager/ScenarioCSV/ScenarioCSV.tsx @@ -8,10 +8,9 @@ import { uploadCSVAndProcess, getCSVForRuleRun } from "@/app/utils/api"; interface ScenarioCSVProps { jsonFile: string; ruleContent?: DecisionGraphType; - ruleVersion?: string | boolean; } -export default function ScenarioCSV({ jsonFile, ruleContent, ruleVersion = "" }: ScenarioCSVProps) { +export default function ScenarioCSV({ jsonFile, ruleContent }: ScenarioCSVProps) { const [file, setFile] = useState(null); const [uploadedFile, setUploadedFile] = useState(false); @@ -30,9 +29,8 @@ export default function ScenarioCSV({ jsonFile, ruleContent, ruleVersion = "" }: }; const handleDownloadScenarios = async () => { - const ruleName = ruleVersion === "draft" ? "Draft" : ruleVersion === "inreview" ? "In Review" : "Published"; try { - const csvContent = await getCSVForRuleRun(jsonFile, ruleName, ruleContent); + const csvContent = await getCSVForRuleRun(jsonFile, ruleContent); message.success(`Scenario Testing Template: ${csvContent}`); } catch (error) { message.error("Error downloading scenarios."); diff --git a/app/components/ScenariosManager/ScenariosManager.tsx b/app/components/ScenariosManager/ScenariosManager.tsx index 4369517..120e399 100644 --- a/app/components/ScenariosManager/ScenariosManager.tsx +++ b/app/components/ScenariosManager/ScenariosManager.tsx @@ -121,7 +121,7 @@ export default function ScenariosManager({ const csvTab = ( - + ); @@ -135,7 +135,6 @@ export default function ScenariosManager({ jsonFile={jsonFile} rulemap={rulemap} ruleContent={ruleContent} - ruleVersion={isEditing} /> - ), + render: (value: string, _: RuleInfo, index: number) => { + return ( + <> + {_.isPublished ? ( + + + + ) : ( + + + + )} + + ); + }, }, { dataIndex: "view", width: "60px", - render: (_: string, { _id }: RuleInfo) => ( - - - - ), + render: (_: string, { _id, isPublished }: RuleInfo) => { + const ruleLink = `/rule/${_id}`; + const draftLink = `${ruleLink}?version=draft`; + return ( + + + + ); + }, }, ]; From 8c96444c6acc07ea42c0079f419ea30912eefc28 Mon Sep 17 00:00:00 2001 From: timwekkenbc Date: Tue, 22 Oct 2024 12:30:12 -0700 Subject: [PATCH 10/12] Fixed issue that caused errors when creating a new rule (was generating schema with no data), Fixed up styling of main page so that all the versions are less squished, Increased max layout size to 1300px --- app/components/SavePublish/SavePublishWarnings.tsx | 2 ++ app/page.tsx | 7 ++++--- app/styles/home.module.css | 9 +++++---- app/styles/layout.module.css | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/components/SavePublish/SavePublishWarnings.tsx b/app/components/SavePublish/SavePublishWarnings.tsx index 0360979..f8759d6 100644 --- a/app/components/SavePublish/SavePublishWarnings.tsx +++ b/app/components/SavePublish/SavePublishWarnings.tsx @@ -28,6 +28,8 @@ export default function SavePublishWarnings({ filePath, ruleContent, isSaving }: const existingKlammInputs = inputOutputSchemaMap.inputs.map(({ field }) => field as string); const existingKlammOutputs = inputOutputSchemaMap.resultOutputs.map(({ field }) => field as string); + if (!ruleContent?.nodes || ruleContent.nodes.length < 2) return; + // Get map the old way for comparrison const generatedSchemaMap = await generateSchemaFromRuleContent(ruleContent); const generatedInputs = generatedSchemaMap.inputs.map(({ field }) => field as string); diff --git a/app/page.tsx b/app/page.tsx index 47424c9..2fb96c8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -98,6 +98,8 @@ export default function Home() { dataIndex: "title", key: "title", sorter: true, + width: "35%", + minWidth: 350, render: (_, record) => { const ruleLink = `/rule/${record._id}`; const draftLink = `${ruleLink}?version=draft`; @@ -131,7 +133,7 @@ export default function Home() { const ruleLink = `/rule/${record._id}`; const draftLink = `${ruleLink}?version=draft`; return ( - + )}