From 0e78a393e112035a30eb635c2be0bc2c08b9f0db Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 3 Sep 2025 11:26:41 -0700 Subject: [PATCH 01/34] initial test page display functionality --- graphql-server/schema.gql | 14 + .../queryFactory/dolt/doltEntityManager.ts | 10 + graphql-server/src/queryFactory/dolt/index.ts | 9 + .../src/queryFactory/doltgres/index.ts | 9 + graphql-server/src/queryFactory/index.ts | 3 + .../src/queryFactory/mysql/index.ts | 4 + graphql-server/src/resolvers.ts | 2 + .../src/systemTables/systemTable.enums.ts | 1 + graphql-server/src/tests/test.model.ts | 40 +++ graphql-server/src/tests/test.resolver.ts | 22 ++ web/renderer/components/DatabaseNav/tabs.ts | 1 + web/renderer/components/DatabaseNav/utils.ts | 3 + .../DatabasePage/ForTests/NewGroupModal.tsx | 49 +++ .../DatabasePage/ForTests/QueryEditor.tsx | 35 ++ .../DatabasePage/ForTests/TestGroup.tsx | 98 ++++++ .../DatabasePage/ForTests/TestItem.tsx | 138 ++++++++ .../DatabasePage/ForTests/TestList.tsx | 160 +++++++++ .../DatabasePage/ForTests/index.module.css | 321 ++++++++++++++++++ .../DatabasePage/ForTests/index.tsx | 26 ++ .../DatabasePage/ForTests/queries.ts | 16 + .../DatabasePage/ForTests/useTestList.ts | 315 +++++++++++++++++ .../pageComponents/DatabasePage/index.tsx | 2 + web/renderer/gen/graphql-types.tsx | 78 +++++ web/renderer/lib/urls.ts | 3 + .../[databaseName]/tests/[refName]/index.tsx | 32 ++ 25 files changed, 1391 insertions(+) create mode 100644 graphql-server/src/tests/test.model.ts create mode 100644 graphql-server/src/tests/test.resolver.ts create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts create mode 100644 web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index fcb679cb6..dad6d8b6a 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -339,6 +339,19 @@ type TagList { list: [Tag!]! } +type Test { + testName: String! + testGroup: String! + testQuery: String! + assertionType: String! + assertionComparator: String! + assertionValue: String! +} + +type TestList { + list: [Test!]! +} + type Query { branch(databaseName: String!, branchName: String!): Branch branchOrDefault(databaseName: String!, branchName: String): Branch @@ -378,6 +391,7 @@ type Query { tables(schemaName: String, refName: String!, databaseName: String!, filterSystemTables: Boolean): [Table!]! tags(databaseName: String!): TagList! tag(databaseName: String!, tagName: String!): Tag + tests(databaseName: String!, refName: String!): TestList! } enum SortBranchesBy { diff --git a/graphql-server/src/queryFactory/dolt/doltEntityManager.ts b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts index 58afd93d3..2ccf4cbed 100644 --- a/graphql-server/src/queryFactory/dolt/doltEntityManager.ts +++ b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts @@ -184,3 +184,13 @@ export async function getDoltRemotesPaginated( .limit(ROW_LIMIT + 1) .getRawMany(); } + +export async function getDoltTests( + em: EntityManager, +): t.PR { + return em + .createQueryBuilder() + .select("*") + .from("dolt_tests", "") + .getRawMany(); +} diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index a13465db6..b11d752e4 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -584,6 +584,14 @@ export class DoltQueryFactory qr.query(qh.callDoltClone, [args.remoteDbPath, args.databaseName]), ); } + + async getTests(args: t.RefArgs): t.PR { + return this.queryForBuilder( + async em => dem.getDoltTests(em), + args.databaseName, + args.refName, + ); +} } async function getTableInfoWithQR( @@ -605,4 +613,5 @@ async function getTableInfoWithQR( foreignKeys: foreignKey.fromDoltRowsRes(fkRows), indexes: index.fromDoltRowsRes(idxRows), }; + } diff --git a/graphql-server/src/queryFactory/doltgres/index.ts b/graphql-server/src/queryFactory/doltgres/index.ts index 816f6be10..c8c3d6cdd 100644 --- a/graphql-server/src/queryFactory/doltgres/index.ts +++ b/graphql-server/src/queryFactory/doltgres/index.ts @@ -559,6 +559,14 @@ export class DoltgresQueryFactory args.databaseName, ); } + + async getTests(args: t.RefArgs): t.PR { + return this.queryForBuilder( + async em => dem.getDoltTests(em), + args.databaseName, + args.refName, + ); + } } async function getTableInfoWithQR( @@ -583,3 +591,4 @@ async function getTableInfoWithQR( indexes: [], }; } + diff --git a/graphql-server/src/queryFactory/index.ts b/graphql-server/src/queryFactory/index.ts index 2d471ef26..e2a57161c 100644 --- a/graphql-server/src/queryFactory/index.ts +++ b/graphql-server/src/queryFactory/index.ts @@ -180,4 +180,7 @@ export declare class QueryFactory { callCreateBranchFromRemote(args: t.RemoteArgs): t.PR; getMergeBase(args: t.RefsArgs): Promise; + + getTests(args: t.RefArgs): t.PR; + } diff --git a/graphql-server/src/queryFactory/mysql/index.ts b/graphql-server/src/queryFactory/mysql/index.ts index 6b86c1f86..e83af5b48 100644 --- a/graphql-server/src/queryFactory/mysql/index.ts +++ b/graphql-server/src/queryFactory/mysql/index.ts @@ -367,4 +367,8 @@ export class MySQLQueryFactory async callCreateBranchFromRemote(_: t.RemoteBranchArgs): t.PR { throw notDoltError("create branch from remote"); } + + async getTests(_: t.RefArgs): t.PR { + throw notDoltError("get tests"); + } } diff --git a/graphql-server/src/resolvers.ts b/graphql-server/src/resolvers.ts index 241a6e7d4..05702181d 100644 --- a/graphql-server/src/resolvers.ts +++ b/graphql-server/src/resolvers.ts @@ -16,6 +16,7 @@ import { StatusResolver } from "./status/status.resolver"; import { TableResolver } from "./tables/table.resolver"; import { FileUploadResolver } from "./tables/upload.resolver"; import { TagResolver } from "./tags/tag.resolver"; +import { TestResolver } from "./tests/test.resolver"; const resolvers = [ BranchResolver, @@ -36,6 +37,7 @@ const resolvers = [ StatusResolver, TableResolver, TagResolver, + TestResolver, ]; export default resolvers; diff --git a/graphql-server/src/systemTables/systemTable.enums.ts b/graphql-server/src/systemTables/systemTable.enums.ts index b5d09e542..9c4a57cb2 100644 --- a/graphql-server/src/systemTables/systemTable.enums.ts +++ b/graphql-server/src/systemTables/systemTable.enums.ts @@ -3,6 +3,7 @@ export enum DoltSystemTable { QUERY_CATALOG = "dolt_query_catalog", SCHEMAS = "dolt_schemas", PROCEDURES = "dolt_procedures", + TESTS = "dolt_tests", } export const systemTableValues = Object.values(DoltSystemTable); diff --git a/graphql-server/src/tests/test.model.ts b/graphql-server/src/tests/test.model.ts new file mode 100644 index 000000000..1d874199f --- /dev/null +++ b/graphql-server/src/tests/test.model.ts @@ -0,0 +1,40 @@ +import { Field, ObjectType } from "@nestjs/graphql"; +import { RawRow } from "../queryFactory/types"; + +@ObjectType() +export class Test { + @Field() + testName: string; + + @Field() + testGroup: string; + + @Field() + testQuery: string; + + @Field() + assertionType: string; + + @Field() + assertionComparator: string; + + @Field() + assertionValue: string; +} + +@ObjectType() +export class TestList { + @Field(_type => [Test]) + list: Test[]; +} + +export function fromDoltRowRes(test: RawRow): Test { + return { + testName: test.test_name, + testGroup: test.test_group, + testQuery: test.test_query, + assertionType: test.assertion_type, + assertionComparator: test.assertion_comparator, + assertionValue: test.assertion_value, + }; +} diff --git a/graphql-server/src/tests/test.resolver.ts b/graphql-server/src/tests/test.resolver.ts new file mode 100644 index 000000000..a03a32697 --- /dev/null +++ b/graphql-server/src/tests/test.resolver.ts @@ -0,0 +1,22 @@ +import { + Args, ArgsType, Field, + Query, + Resolver, +} from "@nestjs/graphql"; +import { ConnectionProvider } from "../connections/connection.provider"; +import { AuthorInfo, RefArgs, TagArgs } from "../utils/commonTypes"; +import { Test, TestList, fromDoltRowRes } from "./test.model"; + +@Resolver(_of => Test) +export class TestResolver { + constructor(private readonly conn: ConnectionProvider) {} + + @Query(_returns => TestList) + async tests(@Args() args: RefArgs): Promise { + const conn = this.conn.connection(); + const res = await conn.getTests(args); + return { + list: res.map(t => fromDoltRowRes(t)), + }; + } +} diff --git a/web/renderer/components/DatabaseNav/tabs.ts b/web/renderer/components/DatabaseNav/tabs.ts index aeba46d92..8e9b74122 100644 --- a/web/renderer/components/DatabaseNav/tabs.ts +++ b/web/renderer/components/DatabaseNav/tabs.ts @@ -5,4 +5,5 @@ export const tabs = [ "Releases", "Pull Requests", "Remotes", + "Tests", ]; diff --git a/web/renderer/components/DatabaseNav/utils.ts b/web/renderer/components/DatabaseNav/utils.ts index 4bd859867..c316d22aa 100644 --- a/web/renderer/components/DatabaseNav/utils.ts +++ b/web/renderer/components/DatabaseNav/utils.ts @@ -10,6 +10,7 @@ import { RefUrl, releases, remotes, + tests, } from "@lib/urls"; function getUrlFromName(name: string): [DatabaseUrl, RefUrl?] { @@ -26,6 +27,8 @@ function getUrlFromName(name: string): [DatabaseUrl, RefUrl?] { return [pulls]; case "Remotes": return [remotes]; + case "Tests": + return [database, tests]; default: return [database, ref]; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx new file mode 100644 index 000000000..b038f8865 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx @@ -0,0 +1,49 @@ +import { Button } from "@dolthub/react-components"; +import css from "./index.module.css"; + +type Props = { + isOpen: boolean; + groupName: string; + onGroupNameChange: (name: string) => void; + onCreateGroup: () => void; + onClose: () => void; +}; + +export default function NewGroupModal({ + isOpen, + groupName, + onGroupNameChange, + onCreateGroup, + onClose +}: Props) { + if (!isOpen) return null; + + const handleClose = () => { + onGroupNameChange(""); + onClose(); + }; + + return ( +
+
+

Create New Test Group

+ onGroupNameChange(e.target.value)} + placeholder="Enter group name..." + onKeyDown={(e) => e.key === 'Enter' && onCreateGroup()} + /> +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor.tsx new file mode 100644 index 000000000..d01ef3a92 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor.tsx @@ -0,0 +1,35 @@ +import dynamic from "next/dynamic"; + +const AceEditor = dynamic(async () => import("@components/AceEditor"), { + ssr: false, +}); + +type Props = { + value: string; + onChange: (value: string) => void; + placeholder?: string; +}; + +export default function QueryEditor({ value, onChange, placeholder }: Props) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx new file mode 100644 index 000000000..0e42b2e30 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx @@ -0,0 +1,98 @@ +import { FaChevronRight } from "@react-icons/all-files/fa/FaChevronRight"; +import { FaPlay } from "@react-icons/all-files/fa/FaPlay"; +import { FaTrash } from "@react-icons/all-files/fa/FaTrash"; +import { Button } from "@dolthub/react-components"; +import { useState, KeyboardEvent, ChangeEvent, MouseEvent } from "react"; +import css from "./index.module.css"; + +type Props = { + group: string; + isExpanded: boolean; + onToggle: () => void; + testCount: number; + groupColor: string; + onRunGroup: () => void; + onDeleteGroup: () => void; + onRenameGroup?: (oldName: string, newName: string) => void; +}; + +export default function TestGroup({ group, isExpanded, onToggle, testCount, groupColor, onRunGroup, onDeleteGroup, onRenameGroup }: Props) { + const groupName = group || "No Group"; + const [localGroupName, setLocalGroupName] = useState(groupName); + const [isEditing, setIsEditing] = useState(false); + + const handleRunGroup = (e: MouseEvent) => { + e.stopPropagation(); + onRunGroup(); + }; + + const handleDeleteGroup = (e: MouseEvent) => { + e.stopPropagation(); + onDeleteGroup(); + }; + + const handleInputChange = (e: ChangeEvent) => { + setLocalGroupName(e.target.value); + }; + + const handleInputBlur = () => { + if (localGroupName.trim() && localGroupName !== groupName) { + onRenameGroup?.(groupName, localGroupName.trim()); + } else { + setLocalGroupName(groupName); + } + setIsEditing(false); + }; + + const handleInputKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleInputBlur(); + } else if (e.key === 'Escape') { + setLocalGroupName(groupName); + setIsEditing(false); + } + }; + + const handleHeaderClick = () => { + if (!isEditing) { + onToggle(); + } + }; + + return ( +
+
+
+ + setIsEditing(true)} + onClick={(e) => e.stopPropagation()} + /> + ({testCount} tests) +
+
+ + + + + + +
+
+
+ ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx new file mode 100644 index 000000000..4f8e501a5 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx @@ -0,0 +1,138 @@ +import { Button } from "@dolthub/react-components"; +import { FaChevronRight } from "@react-icons/all-files/fa/FaChevronRight"; +import { FaPlay } from "@react-icons/all-files/fa/FaPlay"; +import { FaTrash } from "@react-icons/all-files/fa/FaTrash"; +import css from "./index.module.css"; +import QueryEditor from "./QueryEditor"; +import { MouseEvent } from "react"; +import { Test } from "@gen/graphql-types"; + +type Props = { + test: Test; + groupOptions: string[], + isExpanded: boolean; + editingName: string | undefined; + onToggleExpanded: () => void; + onUpdateTest: (field: keyof Test, value: string) => void; + onNameEdit: (name: string) => void; + onNameBlur: () => void; + onRunTest: () => void; + onDeleteTest: () => void; +}; + +export default function TestItem({ + test, + groupOptions, + isExpanded, + editingName, + onToggleExpanded, + onUpdateTest, + onNameEdit, + onNameBlur, + onRunTest, + onDeleteTest +}: Props) { + console.log(groupOptions); + return ( +
  • +
    +
    + + onNameEdit(e.target.value)} + onFocus={() => onNameEdit(test.testName)} + onBlur={onNameBlur} + onClick={(e) => e.stopPropagation()} + placeholder="Test name" + /> +
    +
    + { + e.stopPropagation(); + onRunTest(); + }} + className={`${css.testActionBtn} ${css.runBtn}`} + data-tooltip-content="Run test" + > + + + { + e.stopPropagation(); + onDeleteTest(); + }} + red + className={css.testActionBtn} + data-tooltip-content="Delete test" + > + + +
    +
    + {isExpanded && ( +
    +
    + + +
    +
    + + onUpdateTest('testQuery', value)} + placeholder="Enter SQL query" + /> +
    +
    + + +
    +
    + + +
    +
    + + onUpdateTest('assertionValue', e.target.value)} + placeholder="Expected result" + /> +
    +
    + )} +
  • + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx new file mode 100644 index 000000000..8690b4837 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx @@ -0,0 +1,160 @@ +import HideForNoWritesWrapper from "@components/util/HideForNoWritesWrapper"; +import { Button } from "@dolthub/react-components"; +import { useState } from "react"; +import css from "./index.module.css"; +import NewGroupModal from "./NewGroupModal"; +import TestGroup from "./TestGroup"; +import TestItem from "./TestItem"; +import { useTestList } from "./useTestList"; +import { RefParams } from "@lib/params"; + +type Props = { + params: RefParams; +}; + +export default function TestList({ params }: Props) { + const [showNewGroupModal, setShowNewGroupModal] = useState(false); + const [newGroupName, setNewGroupName] = useState(""); + + const { + expandedItems, + expandedGroups, + editingTestNames, + hasUnsavedChanges, + tests, + groupedTests, + sortedGroupEntries, + toggleExpanded, + toggleGroupExpanded, + updateTest, + handleRunTest, + handleRunGroup, + handleDeleteTest, + handleDeleteGroup, + handleCreateGroup, + handleCreateTest, + handleSaveAll, + handleRenameGroup, + handleTestNameEdit, + handleTestNameBlur, + } = useTestList(params); + + const onCreateGroup = () => { + if (handleCreateGroup(newGroupName, groupedTests)) { + setNewGroupName(""); + setShowNewGroupModal(false); + } + }; + + console.log(tests); + const uniqueGroups = sortedGroupEntries.map(entry => entry[0]).filter(group => group !== "") + + return ( +
    +
    +

    Tests

    +
    + + <> + + + + + +
    +
    + + setShowNewGroupModal(false)} + /> + {tests.length ? ( +
    +
    + {sortedGroupEntries + .filter(([groupName]) => groupName !== "") + .map(([groupName, groupTests]) => { + const isGroupExpanded = expandedGroups.has(groupName); + const groupColor = "#f59e0b"; + + return ( +
    + toggleGroupExpanded(groupName)} + testCount={groupTests.length} + groupColor={groupColor} + onRunGroup={() => handleRunGroup(groupName)} + onDeleteGroup={() => handleDeleteGroup(groupName)} + onRenameGroup={handleRenameGroup} + /> + {isGroupExpanded && ( +
      + {groupTests.map((test) => ( + toggleExpanded(test.testName)} + onUpdateTest={(field, value) => updateTest(test.testName, field, value)} + onNameEdit={(name) => handleTestNameEdit(test.testName, name)} + onNameBlur={() => handleTestNameBlur(test.testName)} + onRunTest={() => handleRunTest(test.testName)} + onDeleteTest={() => handleDeleteTest(test.testName)} + /> + ))} +
    + )} +
    + ); + })} +{(groupedTests[""] ?? []).length > 0 && ( + <> +
    Ungrouped
    +
    +
      + {groupedTests[""].map((test) => ( + toggleExpanded(test.testName)} + onUpdateTest={(field, value) => updateTest(test.testName, field, value)} + onNameEdit={(name) => handleTestNameEdit(test.testName, name)} + onNameBlur={() => handleTestNameBlur(test.testName)} + onRunTest={() => handleRunTest(test.testName)} + onDeleteTest={() => handleDeleteTest(test.testName)} + /> + ))} +
    +
    + + )} +
    +
    + ) : ( +

    + No tests found +

    + )} +
    + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css new file mode 100644 index 000000000..8b5822e3b --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css @@ -0,0 +1,321 @@ +.container { + @apply mx-6; +} + +.top { + @apply flex justify-between pt-6; +} + +.tagContainer { + @apply mr-4 pb-4 text-primary border-stone-100 border-l-[1.5px]; + + @screen lg { + @apply ml-16; + } +} + +.list { + @apply mt-10 pl-6 pr-2; + + @screen lg { + @apply px-10; + } +} + +.item { + @apply border border-stone-50 rounded-lg my-3 py-4 px-6 text-sm relative; +} + +.header { + @apply relative font-semibold pb-2; +} + +.bullet { + @apply border-stone-100 h-3 w-3 absolute bg-white rounded-full border-[1.5px] -left-[1.9rem] top-[0.4rem]; + + @screen lg { + @apply -left-[2.9rem]; + } +} + +.latest { + @apply inline-block px-4 mx-4 rounded-full bg-white border-2 border-green-400 text-green-400 text-sm font-semibold py-[0.05rem]; +} + +.itemTop { + @apply flex justify-between items-center cursor-pointer; +} + +.timeago { + @apply text-storm-200; +} + +.commitId { + @apply font-semibold text-storm-200 mb-1.5; + + &:hover { + @apply text-stone-100; + } + + @screen lg { + @apply mx-4 mb-0; + } +} + +.commitIcon { + @apply hidden; + + @screen lg { + @apply inline; + } +} + +.releaseName { + @apply pb-2 max-w-4xl text-base; +} + +.releaseNotes > p { + @apply py-4; +} + +.loading { + @apply mx-12 my-4; +} + +.noTests { + @apply text-center text-lg m-6; +} + +.expandedContent { + @apply mt-4 p-4 bg-stone-50 rounded border-l-4 border-sky-400; +} + +.fieldGroup { + @apply mb-4; +} + +.fieldLabel { + @apply block text-sm font-semibold mb-2 text-storm-200; +} + +.fieldInput { + @apply w-full p-2 border border-stone-100 rounded text-sm; +} + +.fieldTextarea { + @apply w-full p-2 border border-stone-100 rounded text-sm min-h-[80px]; + resize: vertical; +} + +.fieldSelect { + @apply w-full p-2 border border-stone-100 rounded text-sm bg-white; +} + +.testName { + @apply pb-2 text-base flex items-center; + flex: 1 1 auto; + min-width: 0; +} + +.expandIcon { + @apply mr-2 transition-transform duration-200 text-storm-200; +} + +.expanded .expandIcon { + @apply rotate-90; +} + +/* Test Group Styles */ +.groupHeader { + @apply border-l-4 bg-stone-50 border-stone-300 rounded-r mb-4 cursor-pointer; + transition: all 0.2s ease; +} + +.groupHeader:hover { + @apply bg-stone-50; +} + +.groupExpanded { + @apply mb-2; +} + +.groupHeaderContent { + @apply flex justify-between items-center p-4; +} + +.groupHeaderLeft { + @apply flex items-center cursor-pointer; +} + +.groupHeaderRight { + @apply flex items-center gap-2; +} + +.groupExpandIcon { + @apply mr-3 transition-transform duration-200 text-storm-200; +} + +.groupExpanded .groupExpandIcon { + @apply rotate-90; +} + +.groupName { + @apply text-lg font-semibold text-storm-500 mr-3; +} + +.testCount { + @apply text-sm text-storm-200 font-medium; +} + +.groupColorIndicator { + @apply w-3 h-3 rounded-full; +} + +.groupedItem { + @apply ml-8; +} + +.groupedTests { + @apply mb-6; +} + +.groupedList { + @apply list-none p-0 m-0; +} + +.itemBottom { + @apply text-storm-200 font-normal flex flex-col-reverse; + a { + @apply font-normal; + } + + @screen lg { + @apply block; + } +} + +.bold { + @apply font-semibold; +} + +.delete { + @apply hidden; + + @screen lg { + @apply block ml-2; + } +} + +.button { + @apply mr-4; +} + +/* Action Button Styles */ +.groupActionBtn { + @apply p-2 text-sm rounded opacity-75 transition-opacity duration-200; +} + +.groupActionBtn:hover { + @apply opacity-100; +} + +.testActionBtn { + @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200 ml-1; +} + +.testActionBtn:hover { + @apply opacity-100; +} + +.testActions { + @apply flex items-center gap-1 ml-auto opacity-0 transition-opacity duration-200; +} + +.item:hover .testActions { + @apply opacity-100; +} + +.createButtons { + @apply flex gap-2 mb-6; +} + +.createButtons button { + @apply px-3 py-1.5 text-sm; +} + +.saveActions { + @apply flex gap-2 mt-4 pt-4 border-t border-stone-300; +} + +.saveActions button { + @apply px-2 py-1 text-xs; +} + +.runBtn { + @apply text-green-600; +} + +.runBtn:hover { + @apply text-green-400; +} + +.inlineEditInput { + @apply bg-transparent border border-transparent text-base font-semibold text-storm-500 px-1 py-0.5 m-0 outline-none rounded transition-all; + min-width: 100px; +} + +.inlineEditInput:hover { + @apply bg-stone-50 border-stone-300; +} + +.inlineEditInput:focus { + @apply bg-stone-50 border-sky-400 shadow-sm; +} + +.editableTestName { + @apply bg-transparent border border-transparent text-base px-1 py-0.5 m-0 outline-none rounded transition-all flex-1; + min-width: 150px; + cursor: text; +} + +.editableTestName:hover { + @apply text-sky-600 bg-stone-50 border-stone-300; +} + +.editableTestName:focus { + @apply bg-white border-sky-400 shadow-sm; +} + +.inlineGroupSelect { + @apply bg-transparent border border-stone-300 rounded text-sm px-2 py-1 ml-2 mr-2; + font-size: 13px; + width: auto; + min-width: 120px; +} + +.inlineGroupSelect:focus { + @apply border-sky-400 outline-none; +} + +/* Ungrouped Tests Styles */ +.ungroupedDivider { + @apply font-semibold text-center w-full my-8 text-base text-storm-500; +} + +.ungroupedDivider::before, +.ungroupedDivider::after { + content: ""; + @apply w-[46%] border-t border-stone-300 inline-block align-middle relative; +} + +.ungroupedDivider::after { + @apply right-0 -ml-[55%]; +} + +.ungroupedDivider::before { + @apply left-0 -mr-[55%]; +} + +.ungroupedTests { + @apply mb-6; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx new file mode 100644 index 000000000..c33e5a81b --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx @@ -0,0 +1,26 @@ +import NotDoltWrapper from "@components/util/NotDoltWrapper"; +import { RefParams } from "@lib/params"; +import { tests as testsUrl } from "@lib/urls"; +import ForDefaultBranch from "../ForDefaultBranch"; +import TestList from "./TestList"; + +type Props = { + params: RefParams; +}; + +export default function ForTests(props: Props): JSX.Element { + const feature = "Viewing tests"; + return ( + + + + + + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts new file mode 100644 index 000000000..b096ebe8e --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts @@ -0,0 +1,16 @@ +import { gql } from "@apollo/client"; + +export const LIST_TESTS = gql` + query TestList($databaseName: String!, $refName: String!) { + tests(databaseName: $databaseName, refName: $refName) { + list { + testName + testQuery + testGroup + assertionType + assertionComparator + assertionValue + } + } + } +`; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts new file mode 100644 index 000000000..ad11cc8d5 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts @@ -0,0 +1,315 @@ +import { useState, useMemo, useEffect } from "react"; +import { Test, useTestListQuery } from "@gen/graphql-types"; +import { RefParams } from "@lib/params"; + +// type Test = { +// id: string; +// name: string; +// query: string; +// type: string; +// operator: string; +// expected: string; +// testGroup: string; +// }; + +// const initialTests: Test[] = [ +// { +// id: "1", +// name: "User Count Check", +// query: "SELECT COUNT(*) FROM users", +// type: "Expected Single Value", +// operator: "=", +// expected: "100", +// testGroup: "User Validation" +// }, +// { +// id: "2", +// name: "Active Users Check", +// query: "SELECT COUNT(*) FROM users WHERE active = 1", +// type: "Expected Single Value", +// operator: ">=", +// expected: "50", +// testGroup: "User Validation" +// }, +// { +// id: "3", +// name: "Order Total Check", +// query: "SELECT SUM(total) FROM orders", +// type: "Expected Single Value", +// operator: ">", +// expected: "1000", +// testGroup: "Financial Tests" +// }, +// { +// id: "4", +// name: "Product Inventory", +// query: "SELECT COUNT(*) FROM products WHERE stock > 0", +// type: "Expected Single Value", +// operator: ">", +// expected: "10", +// testGroup: "Inventory Tests" +// }, +// { +// id: "5", +// name: "Revenue by Month", +// query: "SELECT MONTH(created_at), SUM(total) FROM orders GROUP BY MONTH(created_at)", +// type: "Expected Rows", +// operator: ">=", +// expected: "12", +// testGroup: "Financial Tests" +// }, +// { +// id: "6", +// name: "Sample Ungrouped Test", +// query: "SELECT 1", +// type: "Expected Single Value", +// operator: "=", +// expected: "1", +// testGroup: "" +// } +// ]; + +export function useTestList(params: RefParams) { + const { data, loading, error } = useTestListQuery({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + }, + }); + const initialTests = data?.tests.list; + + const [expandedItems, setExpandedItems] = useState>(new Set()); + const [expandedGroups, setExpandedGroups] = useState>( + new Set() + ); + const [editingTestNames, setEditingTestNames] = useState< + Record + >({}); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [tests, setTests] = useState(initialTests || []); + + // Update tests when GraphQL data loads + useEffect(() => { + if (initialTests) { + setTests(initialTests); + } + }, [initialTests]); + + + const toggleExpanded = (id: string) => { + const newExpanded = new Set(expandedItems); + if (newExpanded.has(id)) { + newExpanded.delete(id); + } else { + newExpanded.add(id); + } + setExpandedItems(newExpanded); + }; + + const toggleGroupExpanded = (groupName: string) => { + const newExpandedGroups = new Set(expandedGroups); + if (newExpandedGroups.has(groupName)) { + newExpandedGroups.delete(groupName); + } else { + newExpandedGroups.add(groupName); + } + setExpandedGroups(newExpandedGroups); + }; + + const updateTest = (name: string, field: keyof Test, value: string) => { + setTests( + tests.map((test: Test) => + test.testName === name ? { ...test, [field]: value } : test, + ), + ); + setHasUnsavedChanges(true); + }; + + const handleSaveAll = () => { + console.log("Saving all changes:", tests); + setHasUnsavedChanges(false); + window.alert("All changes saved successfully!"); + }; + + const handleRunTest = (testId: string) => { + if (hasUnsavedChanges) { + handleSaveAll(); + } + console.log(`Running test: ${testId}`); + window.alert(`Running test ${testId}...`); + }; + + const handleRunGroup = (groupName: string) => { + if (hasUnsavedChanges) { + handleSaveAll(); + } + console.log(`Running all tests in group: ${groupName}`); + window.alert(`Running all tests in ${groupName}...`); + }; + + const handleDeleteTest = (testName: string) => { + if (window.confirm("Are you sure you want to delete this test?")) { + setTests(tests.filter(test => test.testName !== testName)); + setExpandedItems(prev => { + const newSet = new Set(prev); + newSet.delete(testName); + return newSet; + }); + setHasUnsavedChanges(true); + } + }; + + const handleDeleteGroup = (groupName: string) => { + if ( + window.confirm( + `Are you sure you want to delete the entire "${groupName}" group and all its tests?`, + ) + ) { + setTests(tests.filter(test => test.testGroup !== groupName)); + setExpandedGroups(prev => { + const newSet = new Set(prev); + newSet.delete(groupName); + return newSet; + }); + setHasUnsavedChanges(true); + } + }; + + const handleCreateGroup = ( + groupName: string, + groupedTests: Record, + ) => { + if ( + groupName.trim() && + !Object.keys(groupedTests).includes(groupName.trim()) + ) { + setExpandedGroups(prev => new Set([...prev, groupName.trim()])); + setHasUnsavedChanges(true); + return true; + } + return false; + }; + + const handleCreateTest = (groupName?: string) => { + const newTest: Test = { + testName: "New Test", + testQuery: "", + assertionType: "expected_single_value", + assertionComparator: "=", + assertionValue: "", + testGroup: groupName ?? "", + }; + + setTests([...tests, newTest]); + setExpandedItems(prev => new Set([...prev, newTest.testName])); + setEditingTestNames(prev => { + return { ...prev, [newTest.testName]: newTest.testName }; + }); + setHasUnsavedChanges(true); + + if (groupName && !expandedGroups.has(groupName)) { + setExpandedGroups(prev => new Set([...prev, groupName])); + } + }; + + const handleRenameGroup = (oldName: string, newName: string) => { + if (newName.trim() && oldName !== newName.trim()) { + setTests( + tests.map(test => + test.testGroup === oldName + ? { ...test, testGroup: newName.trim() } + : test, + ), + ); + + setExpandedGroups(prev => { + const newSet = new Set(prev); + if (newSet.has(oldName)) { + newSet.delete(oldName); + newSet.add(newName.trim()); + } + return newSet; + }); + setHasUnsavedChanges(true); + } + }; + + const handleTestNameEdit = (testId: string, name: string) => { + setEditingTestNames(prev => { + return { + ...prev, + [testId]: name, + }; + }); + }; + + const handleTestNameBlur = (testName: string) => { + const newName = editingTestNames[testName]; + const test = tests.find(t => t.testName === testName); + if (newName.trim() && newName !== test?.testName) { + updateTest(testName, "testName", newName.trim()); + } + setEditingTestNames(prev => { + const newState = { ...prev }; + delete newState[testName]; + return newState; + }); + }; + + const groupedTests = useMemo(() => { + const groups: Record = {}; + tests.forEach(test => { + const groupName = test.testGroup || ""; + groups[groupName] ??= []; + groups[groupName].push(test); + }); + return groups; + }, [tests]); + + const sortedGroupEntries = useMemo(() => { + const entries = Object.entries(groupedTests); + const groupOrder = entries.map(entry => entry[0]); + + entries.sort(([a], [b]) => { + if (a === "" && b === "") return 0; + if (a === "") return 1; + if (b === "") return -1; + + const indexA = groupOrder.indexOf(a); + const indexB = groupOrder.indexOf(b); + + if (indexA !== -1 && indexB !== -1) { + return indexA - indexB; + } + if (indexA !== -1) return -1; + if (indexB !== -1) return 1; + + return a.localeCompare(b); + }); + + return entries; + }, [groupedTests]); + + return { + expandedItems, + expandedGroups, + editingTestNames, + hasUnsavedChanges, + tests, + groupedTests, + sortedGroupEntries, + toggleExpanded, + toggleGroupExpanded, + updateTest, + handleRunTest, + handleRunGroup, + handleDeleteTest, + handleDeleteGroup, + handleCreateGroup, + handleCreateTest, + handleSaveAll, + handleRenameGroup, + handleTestNameEdit, + handleTestNameBlur, + }; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/index.tsx b/web/renderer/components/pageComponents/DatabasePage/index.tsx index ca1eaaddc..94c9fb702 100644 --- a/web/renderer/components/pageComponents/DatabasePage/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/index.tsx @@ -14,6 +14,7 @@ import ForRemotes from "./ForRemotes"; import ForSchema from "./ForSchema"; import ForTable from "./ForTable"; import DatabasePage from "./component"; +import ForTests from "@pageComponents/DatabasePage/ForTests"; export default Object.assign(DatabasePage, { ForBranches, @@ -31,4 +32,5 @@ export default Object.assign(DatabasePage, { ForRemotes, ForSchema, ForTable, + ForTests, }); diff --git a/web/renderer/gen/graphql-types.tsx b/web/renderer/gen/graphql-types.tsx index 7d1de1e6d..df916cd20 100644 --- a/web/renderer/gen/graphql-types.tsx +++ b/web/renderer/gen/graphql-types.tsx @@ -505,6 +505,7 @@ export type Query = { tables: Array; tag?: Maybe; tags: TagList; + tests: TestList; views: Array; }; @@ -757,6 +758,12 @@ export type QueryTagsArgs = { }; +export type QueryTestsArgs = { + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}; + + export type QueryViewsArgs = { databaseName: Scalars['String']['input']; refName: Scalars['String']['input']; @@ -914,6 +921,21 @@ export type TagList = { list: Array; }; +export type Test = { + __typename?: 'Test'; + assertionComparator: Scalars['String']['output']; + assertionType: Scalars['String']['output']; + assertionValue: Scalars['String']['output']; + testGroup: Scalars['String']['output']; + testName: Scalars['String']['output']; + testQuery: Scalars['String']['output']; +}; + +export type TestList = { + __typename?: 'TestList'; + list: Array; +}; + export type TextDiff = { __typename?: 'TextDiff'; leftLines: Scalars['String']['output']; @@ -1509,6 +1531,14 @@ export type PushToRemoteMutationVariables = Exact<{ export type PushToRemoteMutation = { __typename?: 'Mutation', pushToRemote: { __typename?: 'PushRes', success: boolean, message: string } }; +export type TestListQueryVariables = Exact<{ + databaseName: Scalars['String']['input']; + refName: Scalars['String']['input']; +}>; + + +export type TestListQuery = { __typename?: 'Query', tests: { __typename?: 'TestList', list: Array<{ __typename?: 'Test', testName: string, testQuery: string, testGroup: string, assertionType: string, assertionComparator: string, assertionValue: string }> } }; + export type LoadDataMutationVariables = Exact<{ databaseName: Scalars['String']['input']; refName: Scalars['String']['input']; @@ -4462,6 +4492,54 @@ export function usePushToRemoteMutation(baseOptions?: Apollo.MutationHookOptions export type PushToRemoteMutationHookResult = ReturnType; export type PushToRemoteMutationResult = Apollo.MutationResult; export type PushToRemoteMutationOptions = Apollo.BaseMutationOptions; +export const TestListDocument = gql` + query TestList($databaseName: String!, $refName: String!) { + tests(databaseName: $databaseName, refName: $refName) { + list { + testName + testQuery + testGroup + assertionType + assertionComparator + assertionValue + } + } +} + `; + +/** + * __useTestListQuery__ + * + * To run a query within a React component, call `useTestListQuery` and pass it any options that fit your needs. + * When your component renders, `useTestListQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useTestListQuery({ + * variables: { + * databaseName: // value for 'databaseName' + * refName: // value for 'refName' + * }, + * }); + */ +export function useTestListQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: TestListQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(TestListDocument, options); + } +export function useTestListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(TestListDocument, options); + } +export function useTestListSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(TestListDocument, options); + } +export type TestListQueryHookResult = ReturnType; +export type TestListLazyQueryHookResult = ReturnType; +export type TestListSuspenseQueryHookResult = ReturnType; +export type TestListQueryResult = Apollo.QueryResult; export const LoadDataDocument = gql` mutation LoadData($databaseName: String!, $refName: String!, $schemaName: String, $tableName: String!, $importOp: ImportOperation!, $fileType: FileType!, $file: Upload!, $modifier: LoadDataModifier) { loadDataFile( diff --git a/web/renderer/lib/urls.ts b/web/renderer/lib/urls.ts index 35b3d9bdd..4f06205c7 100644 --- a/web/renderer/lib/urls.ts +++ b/web/renderer/lib/urls.ts @@ -103,6 +103,9 @@ function getDiffRange(p: ps.DiffParams): string { export const releases = (p: ps.OptionalRefParams): Route => database(p).addStatic("releases").withQuery({ refName: p.refName }); +export const tests = (p: ps.RefParams): Route => + database(p).addStatic("tests").addDynamic("refName", p.refName, ENCODE); + export const remotes = (p: ps.OptionalRefParams): Route => database(p).addStatic("remotes").withQuery({ refName: p.refName }); diff --git a/web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx b/web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx new file mode 100644 index 000000000..e6f8cb3e0 --- /dev/null +++ b/web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx @@ -0,0 +1,32 @@ +import Page from "@components/util/Page"; +import { RefParams } from "@lib/params"; +import DatabasePage from "@pageComponents/DatabasePage"; +import { GetServerSideProps, NextPage } from "next"; + +type Props = { + params: RefParams; +}; + +const DatabaseTestsPage: NextPage = ({ params }) => ( + + + +); + +// #!if !isElectron +export const getServerSideProps: GetServerSideProps = async ({ + params, + }) => { + return { + props: { params: params as RefParams }, + }; +}; + +// #!endif + +export default DatabaseTestsPage; From 6fc6c4aaff083c690d5d74b9ce3741a5ea56709e Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Fri, 5 Sep 2025 10:22:12 -0700 Subject: [PATCH 02/34] initial run tests functionality --- graphql-server/schema.gql | 31 ++++ .../queryFactory/dolt/doltEntityManager.ts | 35 +++- graphql-server/src/queryFactory/dolt/index.ts | 20 ++- .../src/queryFactory/dolt/queries.ts | 5 + .../src/queryFactory/doltgres/index.ts | 20 ++- .../src/queryFactory/doltgres/queries.ts | 5 + graphql-server/src/queryFactory/index.ts | 6 +- .../src/queryFactory/mysql/index.ts | 10 +- graphql-server/src/queryFactory/types.ts | 26 +++ graphql-server/src/tests/test.model.ts | 27 ++- graphql-server/src/tests/test.resolver.ts | 71 +++++++- graphql-server/src/utils/commonTypes.ts | 1 + .../DatabasePage/ForTests/NewGroupModal.tsx | 2 +- .../DatabasePage/ForTests/TestGroup.tsx | 24 ++- .../DatabasePage/ForTests/TestItem.tsx | 31 +++- .../DatabasePage/ForTests/TestList.tsx | 6 +- .../DatabasePage/ForTests/queries.ts | 29 ++++ .../DatabasePage/ForTests/useTestList.ts | 137 +++++++++++++-- web/renderer/gen/graphql-types.tsx | 158 ++++++++++++++++++ 19 files changed, 612 insertions(+), 32 deletions(-) diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index dad6d8b6a..b374424b0 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -352,6 +352,18 @@ type TestList { list: [Test!]! } +type TestResult { + testName: String! + testGroupName: String! + query: String! + status: String! + message: String! +} + +type TestResultList { + list: [TestResult!]! +} + type Query { branch(databaseName: String!, branchName: String!): Branch branchOrDefault(databaseName: String!, branchName: String): Branch @@ -392,6 +404,7 @@ type Query { tags(databaseName: String!): TagList! tag(databaseName: String!, tagName: String!): Tag tests(databaseName: String!, refName: String!): TestList! + runTests(refName: String!, databaseName: String!, identifiers: TestIdentifierArgs): TestResultList! } enum SortBranchesBy { @@ -419,6 +432,10 @@ enum DiffRowType { All } +input TestIdentifierArgs { + values: [String!]! +} + type Mutation { createBranch(databaseName: String!, newBranchName: String!, fromRefName: String!): String! deleteBranch(databaseName: String!, branchName: String!): Boolean! @@ -440,6 +457,7 @@ type Mutation { restoreAllTables(databaseName: String!, refName: String!): Boolean! createTag(tagName: String!, databaseName: String!, message: String, fromRefName: String!, author: AuthorInfo): String! deleteTag(databaseName: String!, tagName: String!): Boolean! + saveTests(refName: String!, databaseName: String!, tests: TestListArgs!): TestList! } enum ImportOperation { @@ -463,4 +481,17 @@ enum LoadDataModifier { input AuthorInfo { name: String! email: String! +} + +input TestListArgs { + list: [TestArgs!]! +} + +input TestArgs { + testName: String! + testGroup: String! + testQuery: String! + assertionType: String! + assertionComparator: String! + assertionValue: String! } \ No newline at end of file diff --git a/graphql-server/src/queryFactory/dolt/doltEntityManager.ts b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts index 2ccf4cbed..08a0243e5 100644 --- a/graphql-server/src/queryFactory/dolt/doltEntityManager.ts +++ b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts @@ -1,4 +1,4 @@ -import { EntityManager } from "typeorm"; +import { EntityManager, InsertResult } from "typeorm"; import { SchemaType } from "../../schemas/schema.enums"; import { SchemaItem } from "../../schemas/schema.model"; import { DoltSystemTable } from "../../systemTables/systemTable.enums"; @@ -6,6 +6,7 @@ import { ROW_LIMIT, handleTableNotFound } from "../../utils"; import { tableWithSchema } from "../postgres/utils"; import * as t from "../types"; import { getOrderByColForBranches } from "./queries"; +import { TestArgs } from "../types"; export async function getDoltSchemas( em: EntityManager, @@ -194,3 +195,35 @@ export async function getDoltTests( .from("dolt_tests", "") .getRawMany(); } + +export async function saveDoltTests( + em: EntityManager, + tests: TestArgs[] +): Promise { + return em.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager + .createQueryBuilder() + .delete() + .from("dolt_tests") + .execute(); + + return transactionalEntityManager + .createQueryBuilder() + .insert() + .into("dolt_tests") + .values( + tests.map(t => { + return { + test_name: t.testName, + test_group: t.testGroup, + test_query: t.testQuery, + assertion_type: t.assertionType, + assertion_comparator: t.assertionComparator, + assertion_value: t.assertionValue + } + } + ) + ) + .execute(); + }); +} diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index b11d752e4..d0550b37a 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -16,6 +16,7 @@ import * as t from "../types"; import * as dem from "./doltEntityManager"; import * as qh from "./queries"; import { getAuthorString, handleRefNotFound, unionCols } from "./utils"; +import { InsertResult } from "typeorm"; export class DoltQueryFactory extends MySQLQueryFactory @@ -591,7 +592,24 @@ export class DoltQueryFactory args.databaseName, args.refName, ); -} + } + + async runTests(args: t.RunTestsArgs): t.PR { + return this.query( + qh.doltTestRun(args.identifiers?.values.length ?? 0), + args.identifiers?.values, + args.databaseName, + args.refName, + ) + } + + async saveTests(args: t.SaveTestsArgs): Promise { + return this.queryForBuilder( + async em => dem.saveDoltTests(em, args.tests.list), + args.databaseName, + args.refName, + ) + } } async function getTableInfoWithQR( diff --git a/graphql-server/src/queryFactory/dolt/queries.ts b/graphql-server/src/queryFactory/dolt/queries.ts index a6f1872bb..7191706d6 100644 --- a/graphql-server/src/queryFactory/dolt/queries.ts +++ b/graphql-server/src/queryFactory/dolt/queries.ts @@ -181,3 +181,8 @@ export const callResetHard = `CALL DOLT_RESET("--hard")`; export const callCheckoutTable = `CALL DOLT_CHECKOUT(?)`; export const callDoltClone = `CALL DOLT_CLONE(?,?)`; + +export const doltTestRun = (argCount: number) => { + const placeholders = Array.from({ length: argCount }, () => `?`).join(', '); + return `SELECT * FROM DOLT_TEST_RUN(${placeholders})`; +}; diff --git a/graphql-server/src/queryFactory/doltgres/index.ts b/graphql-server/src/queryFactory/doltgres/index.ts index c8c3d6cdd..0ab7650eb 100644 --- a/graphql-server/src/queryFactory/doltgres/index.ts +++ b/graphql-server/src/queryFactory/doltgres/index.ts @@ -1,4 +1,4 @@ -import { QueryRunner } from "typeorm"; +import { InsertResult, QueryRunner } from "typeorm"; import { QueryFactory } from ".."; import * as column from "../../columns/column.model"; import { CommitDiffType } from "../../diffSummaries/diffSummary.enums"; @@ -567,8 +567,26 @@ export class DoltgresQueryFactory args.refName, ); } + + async runTests(args: t.RunTestsArgs): t.PR { + return this.query( + qh.doltTestRun(args.identifiers?.values.length ?? 0), + args.identifiers?.values, + args.databaseName, + args.refName, + ) + } + + async saveTests(args: t.SaveTestsArgs): Promise { + return this.queryForBuilder( + async em => dem.saveDoltTests(em, args.tests.list), + args.databaseName, + args.refName, + ) + } } + async function getTableInfoWithQR( qr: QueryRunner, args: t.TableMaybeSchemaArgs, diff --git a/graphql-server/src/queryFactory/doltgres/queries.ts b/graphql-server/src/queryFactory/doltgres/queries.ts index 79d5b17a4..2c22b0414 100644 --- a/graphql-server/src/queryFactory/doltgres/queries.ts +++ b/graphql-server/src/queryFactory/doltgres/queries.ts @@ -192,3 +192,8 @@ export const callPushRemote = `SELECT DOLT_PUSH($1::text, $2::text)`; export const callFetchRemote = `SELECT DOLT_FETCH($1::text)`; export const callCreateBranchFromRemote = `CALL DOLT_BRANCH($1::text, $2::text)`; + +export const doltTestRun = (argCount: number) => { + const placeholders = Array.from({ length: argCount }, (_, i) => `$${i + 1}::text`).join(', '); + return `SELECT * FROM DOLT_TEST_RUN(${placeholders})`; +} diff --git a/graphql-server/src/queryFactory/index.ts b/graphql-server/src/queryFactory/index.ts index e2a57161c..0c1092a8f 100644 --- a/graphql-server/src/queryFactory/index.ts +++ b/graphql-server/src/queryFactory/index.ts @@ -1,4 +1,4 @@ -import { DataSource, EntityManager, QueryRunner } from "typeorm"; +import { DataSource, EntityManager, InsertResult, QueryRunner } from "typeorm"; import { CommitDiffType } from "../diffSummaries/diffSummary.enums"; import { SchemaType } from "../schemas/schema.enums"; import { SchemaItem } from "../schemas/schema.model"; @@ -183,4 +183,8 @@ export declare class QueryFactory { getTests(args: t.RefArgs): t.PR; + runTests(args: t.RunTestsArgs): t.PR; + + saveTests(args: t.SaveTestsArgs): Promise; + } diff --git a/graphql-server/src/queryFactory/mysql/index.ts b/graphql-server/src/queryFactory/mysql/index.ts index e83af5b48..f944a35dc 100644 --- a/graphql-server/src/queryFactory/mysql/index.ts +++ b/graphql-server/src/queryFactory/mysql/index.ts @@ -1,4 +1,4 @@ -import { EntityManager, QueryRunner } from "typeorm"; +import { EntityManager, InsertResult, QueryRunner } from "typeorm"; import { QueryFactory } from ".."; import { SchemaType } from "../../schemas/schema.enums"; import { SchemaItem } from "../../schemas/schema.model"; @@ -371,4 +371,12 @@ export class MySQLQueryFactory async getTests(_: t.RefArgs): t.PR { throw notDoltError("get tests"); } + + async runTests(_: t.RefArgs): t.PR { + throw notDoltError("run tests"); + } + + async saveTests(_: t.SaveTestsArgs): Promise { + throw notDoltError("save tests"); + } } diff --git a/graphql-server/src/queryFactory/types.ts b/graphql-server/src/queryFactory/types.ts index 327f6c46e..136191f19 100644 --- a/graphql-server/src/queryFactory/types.ts +++ b/graphql-server/src/queryFactory/types.ts @@ -1,5 +1,7 @@ import { SortBranchesBy } from "../branches/branch.enum"; import { DiffRowType } from "../rowDiffs/rowDiff.enums"; +import { Field } from "@nestjs/graphql"; +import { TestList } from "../tests/test.model"; export type DBArgs = { databaseName: string }; export type CloneArgs = DBArgs & { remoteDbPath: string }; @@ -68,3 +70,27 @@ export type TableRowPagination = { pkCols: string[]; offset: number }; export type DiffRes = Promise<{ colsUnion: RawRows; diff: RawRows }>; export type CommitsRes = Promise<{ fromCommitId: string; toCommitId: string }>; export type CommitAuthor = { name: string; email: string }; + +export type TestArgs = { + testName: string; + testGroup: string; + testQuery: string; + assertionType: string; + assertionComparator: string; + assertionValue: string; +} + +export type TestListArgs = { + list: TestArgs[]; +} +export type SaveTestsArgs = RefArgs & { + tests: TestListArgs; +} + +export type TestIdentifierArgs = { + values: string[]; +} + +export type RunTestsArgs = RefArgs & { + identifiers?: TestIdentifierArgs; +} diff --git a/graphql-server/src/tests/test.model.ts b/graphql-server/src/tests/test.model.ts index 1d874199f..5cfff5fc7 100644 --- a/graphql-server/src/tests/test.model.ts +++ b/graphql-server/src/tests/test.model.ts @@ -1,5 +1,6 @@ import { Field, ObjectType } from "@nestjs/graphql"; import { RawRow } from "../queryFactory/types"; +import { ObjectLiteral } from "typeorm"; @ObjectType() export class Test { @@ -28,7 +29,31 @@ export class TestList { list: Test[]; } -export function fromDoltRowRes(test: RawRow): Test { +@ObjectType() +export class TestResult { + @Field() + testName: string; + + @Field() + testGroupName: string; + + @Field() + query: string; + + @Field() + status: string; + + @Field() + message: string; +} + +@ObjectType() +export class TestResultList { + @Field(_type => [TestResult]) + list: TestResult[]; +} + +export function fromDoltRowRes(test: RawRow | ObjectLiteral): Test { return { testName: test.test_name, testGroup: test.test_group, diff --git a/graphql-server/src/tests/test.resolver.ts b/graphql-server/src/tests/test.resolver.ts index a03a32697..0ca37af14 100644 --- a/graphql-server/src/tests/test.resolver.ts +++ b/graphql-server/src/tests/test.resolver.ts @@ -1,11 +1,57 @@ import { - Args, ArgsType, Field, + Args, ArgsType, Field, InputType, Mutation, Query, Resolver, } from "@nestjs/graphql"; import { ConnectionProvider } from "../connections/connection.provider"; -import { AuthorInfo, RefArgs, TagArgs } from "../utils/commonTypes"; -import { Test, TestList, fromDoltRowRes } from "./test.model"; +import { RefArgs } from "../utils/commonTypes"; +import { Test, TestList, fromDoltRowRes, TestResultList } from "./test.model"; + + +@InputType() +class TestArgs { + @Field() + testName: string; + + @Field() + testGroup: string; + + @Field() + testQuery: string; + + @Field() + assertionType: string; + + @Field() + assertionComparator: string; + + @Field() + assertionValue: string; +} + +@InputType() +class TestListArgs { + @Field(_type => [TestArgs]) + list: TestArgs[]; +} + +@ArgsType() +class SaveTestsArgs extends RefArgs { + @Field() + tests: TestListArgs; +} + +@InputType() +class TestIdentifierArgs { + @Field(_type => [String]) + values: string[]; +} + +@ArgsType() +class RunTestsArgs extends RefArgs { + @Field({ nullable: true }) + identifiers?: TestIdentifierArgs; +} @Resolver(_of => Test) export class TestResolver { @@ -19,4 +65,23 @@ export class TestResolver { list: res.map(t => fromDoltRowRes(t)), }; } + + @Query(_returns => TestResultList) + async runTests(@Args() args: RunTestsArgs) { + const conn = this.conn.connection(); + const res = await conn.runTests(args); + return { + list: res.map(t => fromDoltRowRes(t)), + }; + } + + @Mutation(_returns => TestList) + async saveTests(@Args() args: SaveTestsArgs): Promise { + const conn = this.conn.connection(); + const res = await conn.saveTests(args); + console.dir(res, { depth: null }); + return { + list: res.generatedMaps.map(t => fromDoltRowRes(t)), + }; + } } diff --git a/graphql-server/src/utils/commonTypes.ts b/graphql-server/src/utils/commonTypes.ts index 950d0d160..17ecb30fa 100644 --- a/graphql-server/src/utils/commonTypes.ts +++ b/graphql-server/src/utils/commonTypes.ts @@ -98,3 +98,4 @@ export class AuthorInfo { @Field() email: string; } + diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx index b038f8865..627667af6 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx @@ -25,7 +25,7 @@ export default function NewGroupModal({ return (
    -
    +

    Create New Test Group

    void; testCount: number; groupColor: string; + groupResult?: 'passed' | 'failed'; onRunGroup: () => void; onDeleteGroup: () => void; onRenameGroup?: (oldName: string, newName: string) => void; }; -export default function TestGroup({ group, isExpanded, onToggle, testCount, groupColor, onRunGroup, onDeleteGroup, onRenameGroup }: Props) { +export default function TestGroup({ group, isExpanded, onToggle, testCount, groupColor, groupResult, onRunGroup, onDeleteGroup, onRenameGroup }: Props) { const groupName = group || "No Group"; const [localGroupName, setLocalGroupName] = useState(groupName); const [isEditing, setIsEditing] = useState(false); @@ -76,6 +79,25 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, grou ({testCount} tests)
    + {groupResult && ( +
    + {groupResult === 'passed' ? ( + <> + + Passed + + ) : ( + <> + + Failed + + )} +
    + )} void; onUpdateTest: (field: keyof Test, value: string) => void; onNameEdit: (name: string) => void; @@ -25,6 +28,7 @@ export default function TestItem({ groupOptions, isExpanded, editingName, + testResult, onToggleExpanded, onUpdateTest, onNameEdit, @@ -32,9 +36,8 @@ export default function TestItem({ onRunTest, onDeleteTest }: Props) { - console.log(groupOptions); return ( -
  • +
  • @@ -48,6 +51,25 @@ export default function TestItem({ placeholder="Test name" />
    + {testResult && ( +
    + {testResult.status === 'passed' ? ( + <> + + Passed + + ) : ( + <> + + Failed + + )} +
    + )}
    { @@ -74,6 +96,11 @@ export default function TestItem({
    {isExpanded && (
    + {testResult?.status === 'failed' && testResult.error && ( +
    + Error: {testResult.error} +
    + )}
    ); -} \ No newline at end of file +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx index d0cccc205..be5374081 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx @@ -6,6 +6,7 @@ import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; import { Button } from "@dolthub/react-components"; import { useState, KeyboardEvent, ChangeEvent, MouseEvent } from "react"; import css from "./index.module.css"; +import ConfirmationModal from "./ConfirmationModal"; type Props = { group: string; @@ -23,17 +24,27 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, grou const groupName = group || "No Group"; const [localGroupName, setLocalGroupName] = useState(groupName); const [isEditing, setIsEditing] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const handleRunGroup = (e: MouseEvent) => { e.stopPropagation(); onRunGroup(); }; - const handleDeleteGroup = (e: MouseEvent) => { + const handleDeleteClick = (e: MouseEvent) => { e.stopPropagation(); + setShowDeleteConfirm(true); + }; + + const handleConfirmDelete = () => { + setShowDeleteConfirm(false); onDeleteGroup(); }; + const handleCancelDelete = () => { + setShowDeleteConfirm(false); + }; + const handleInputChange = (e: ChangeEvent) => { setLocalGroupName(e.target.value); }; @@ -107,15 +118,26 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, grou
    + +
  • ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx index efb724906..eacca33ea 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx @@ -1,4 +1,4 @@ -import { Button } from "@dolthub/react-components"; +import { Button, FormSelect, FormInput } from "@dolthub/react-components"; import { FaChevronRight } from "@react-icons/all-files/fa/FaChevronRight"; import { FaPlay } from "@react-icons/all-files/fa/FaPlay"; import { FaTrash } from "@react-icons/all-files/fa/FaTrash"; @@ -6,8 +6,9 @@ import { FaCheck } from "@react-icons/all-files/fa/FaCheck"; import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; import css from "./index.module.css"; import QueryEditor from "./QueryEditor"; -import { MouseEvent } from "react"; +import { MouseEvent, useState } from "react"; import { Test } from "@gen/graphql-types"; +import ConfirmationModal from "./ConfirmationModal"; type Props = { test: Test; @@ -36,6 +37,22 @@ export default function TestItem({ onRunTest, onDeleteTest }: Props) { + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + const handleDeleteClick = (e: MouseEvent) => { + e.stopPropagation(); + setShowDeleteConfirm(true); + }; + + const handleConfirmDelete = () => { + setShowDeleteConfirm(false); + onDeleteTest(); + }; + + const handleCancelDelete = () => { + setShowDeleteConfirm(false); + }; + return (
  • @@ -82,10 +99,7 @@ export default function TestItem({ { - e.stopPropagation(); - onDeleteTest(); - }} + onClick={handleDeleteClick} red className={css.testActionBtn} data-tooltip-content="Delete test" @@ -97,22 +111,23 @@ export default function TestItem({ {isExpanded && (
    {testResult?.status === 'failed' && testResult.error && ( -
    +
    Error: {testResult.error}
    )}
    - - + option !== "").map(option => {return { + value: option, + label: option + }}) + ]} + val={test.testGroup || ""} + onChangeValue={(value) => onUpdateTest('testGroup', value || "")} + />
    @@ -123,43 +138,55 @@ export default function TestItem({ />
    - - + onUpdateTest('assertionType', value || "")} + />
    - - + ", label: ">" }, + { value: "<", label: "<" }, + { value: ">=", label: ">=" }, + { value: "<=", label: "<=" } + ]} + val={test.assertionComparator} + onChangeValue={(value) => onUpdateTest('assertionComparator', value || "")} + />
    - - onUpdateTest('assertionValue', e.target.value)} + onChangeString={(value) => onUpdateTest('assertionValue', value)} placeholder="Expected result" + className={css.fullWidthFormInput} + style={{ width: '100%' }} />
    )} + +
  • ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx index 532c49162..a414ce7df 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx @@ -5,6 +5,7 @@ import css from "./index.module.css"; import NewGroupModal from "./NewGroupModal"; import TestGroup from "./TestGroup"; import TestItem from "./TestItem"; +import ConfirmationModal from "./ConfirmationModal"; import { useTestList } from "./useTestList"; import { RefParams } from "@lib/params"; @@ -25,6 +26,8 @@ export default function TestList({ params }: Props) { groupedTests, sortedGroupEntries, testResults, + showUnsavedModal, + pendingNavigation, getGroupResult, toggleExpanded, toggleGroupExpanded, @@ -40,8 +43,13 @@ export default function TestList({ params }: Props) { handleRenameGroup, handleTestNameEdit, handleTestNameBlur, + handleConfirmNavigation, + handleCancelNavigation, } = useTestList(params); + // Debug: Log testResults to see if they're being received in the UI + console.log('DEBUG TestList: testResults state:', testResults); + const onCreateGroup = () => { if (handleCreateGroup(newGroupName, groupedTests)) { setNewGroupName(""); @@ -168,6 +176,17 @@ export default function TestList({ params }: Props) { No tests found

    )} + +
    ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css index 8b5822e3b..bed35857c 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css @@ -111,6 +111,13 @@ @apply w-full p-2 border border-stone-100 rounded text-sm bg-white; } +.fullWidthFormInput { + @apply w-full !important; + width: 100% !important; + min-width: 100% !important; + max-width: 100% !important; +} + .testName { @apply pb-2 text-base flex items-center; flex: 1 1 auto; @@ -151,6 +158,22 @@ @apply flex items-center gap-2; } +.groupActionBtn { + @apply p-2 text-sm rounded opacity-75 transition-opacity duration-200; +} + +.groupActionBtn.deleteBtn { + @apply opacity-0 transition-opacity duration-200; +} + +.groupHeader:hover .groupActionBtn.deleteBtn { + @apply opacity-75; +} + +.groupActionBtn.deleteBtn:hover { + @apply opacity-100; +} + .groupExpandIcon { @apply mr-3 transition-transform duration-200 text-storm-200; } @@ -211,13 +234,6 @@ } /* Action Button Styles */ -.groupActionBtn { - @apply p-2 text-sm rounded opacity-75 transition-opacity duration-200; -} - -.groupActionBtn:hover { - @apply opacity-100; -} .testActionBtn { @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200 ml-1; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts index fc41a8f19..53a47e38d 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts @@ -1,8 +1,10 @@ -import { useState, useMemo, useEffect } from "react"; +import { useState, useMemo, useEffect, useRef } from "react"; import { Test, useRunTestsLazyQuery, useSaveTestsMutation, useTestListQuery } from "@gen/graphql-types"; import { RefParams } from "@lib/params"; +import { useRouter } from "next/router"; export function useTestList(params: RefParams) { + const router = useRouter(); const { data } = useTestListQuery({ variables: { databaseName: params.databaseName, @@ -20,15 +22,129 @@ export function useTestList(params: RefParams) { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [tests, setTests] = useState([]); const [emptyGroups, setEmptyGroups] = useState>(new Set()); - const [testResults, setTestResults] = useState>({}); - + const [testResults, setTestResults] = useState>({}); + const [showUnsavedModal, setShowUnsavedModal] = useState(false); + const [pendingNavigation, setPendingNavigation] = useState(null); + const autoRunExecutedRef = useRef(false); + // Update tests when GraphQL data loads useEffect(() => { if (data?.tests.list) { const initialTests = data.tests.list.map(({ __typename, ...test }) => test); setTests(initialTests); + }}, [data?.tests.list]); + + const handleConfirmNavigation = () => { + if (pendingNavigation) { + setShowUnsavedModal(false); + + const url = pendingNavigation; + setPendingNavigation(null); + setHasUnsavedChanges(false); // Clear unsaved changes to allow navigation + + // Use setTimeout to ensure state updates are processed + setTimeout(async () => { + await router.push(url); + }); } - }, [data?.tests.list]); + }; + + const handleCancelNavigation = () => { + setShowUnsavedModal(false); + setPendingNavigation(null); + }; + + // Warn about unsaved changes when navigating away + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (hasUnsavedChanges) { + e.preventDefault(); + e.returnValue = 'You have unsaved changes. Are you sure you want to leave?'; + return 'You have unsaved changes. Are you sure you want to leave?'; + } + }; + + const handleRouteChangeStart = (url: string) => { + if (hasUnsavedChanges && router.asPath !== url) { + setPendingNavigation(url); + setShowUnsavedModal(true); + router.events.emit('routeChangeError'); + throw 'Route change aborted by user'; + } + }; + + // Handle browser navigation (refresh, close tab, etc.) + window.addEventListener('beforeunload', handleBeforeUnload); + + // Handle Next.js client-side navigation + router.events.on('routeChangeStart', handleRouteChangeStart); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + router.events.off('routeChangeStart', handleRouteChangeStart); + }; + }, [hasUnsavedChanges, router]); + + // Auto-run tests if runTests query parameter is present + useEffect(() => { + const shouldRunTests = router.query.runTests === 'true'; + console.log('DEBUG: Auto-run check:', { + shouldRunTests, + testsLength: tests.length, + autoRunExecuted: autoRunExecutedRef.current, + routerQuery: router.query, + pathname: router.pathname + }); + + // Reset the flag when runTests param is present to allow re-running + if (shouldRunTests) { + autoRunExecutedRef.current = false; + } + + if (shouldRunTests && tests.length > 0 && !autoRunExecutedRef.current) { + console.log('DEBUG: Starting auto-run of tests'); + // Run all tests automatically + const runAllTests = async () => { + autoRunExecutedRef.current = true; // Prevent re-running + console.log('DEBUG: Executing runTests GraphQL mutation'); + try { + const result = await runTests({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + }, + }); + + console.log('DEBUG: runTests result:', result); + const testResultsList = result.data?.runTests.list || []; + const allResults: Record = {}; + + for (const testResult of testResultsList) { + if (testResult.status === 'PASS') { + allResults[testResult.testName] = { + status: 'passed' + } + } else { + allResults[testResult.testName] = { + status: 'failed', + error: testResult.message + } + } + } + + console.log('DEBUG: Setting test results:', allResults); + setTestResults(allResults); + + } catch (error) { + console.error('Error auto-running tests:', error); + } + }; + + void runAllTests(); + } + + // Keep the runTests query parameter to preserve the auto-run behavior + }, [router.query.runTests, tests.length, router, runTests, params.databaseName, params.refName, data?.tests.list]); const [saveTestsMutation] = useSaveTestsMutation({ variables: { @@ -181,17 +297,17 @@ export function useTestList(params: RefParams) { }}); }; - const handleDeleteTest = (testName: string) => { + const handleDeleteTest = async (testName: string) => { setTests(tests.filter(test => test.testName !== testName)); setExpandedItems(prev => { const newSet = new Set(prev); newSet.delete(testName); return newSet; }); - setHasUnsavedChanges(true); + await handleSaveAll(); }; - const handleDeleteGroup = (groupName: string) => { + const handleDeleteGroup = async (groupName: string) => { setTests(tests.filter(test => test.testGroup !== groupName)); setExpandedGroups(prev => { const newSet = new Set(prev); @@ -203,7 +319,7 @@ export function useTestList(params: RefParams) { newSet.delete(groupName); return newSet; }); - setHasUnsavedChanges(true); + await handleSaveAll(); } const handleCreateGroup = ( @@ -232,7 +348,7 @@ export function useTestList(params: RefParams) { while (existingNames.includes(uniqueTestName)) { uniqueTestName = `${baseTestName} ${counter}`; - counter++; + counter += 1; } const newTest: Test = { @@ -356,7 +472,7 @@ export function useTestList(params: RefParams) { }, [groupedTests]); const getGroupResult = (groupName: string) => { - const groupTests = groupedTests[groupName] || []; + const groupTests = groupedTests[groupName]; if (groupTests.length === 0) return undefined; const results = groupTests.map(test => testResults[test.testName]); @@ -364,8 +480,8 @@ export function useTestList(params: RefParams) { // If any test doesn't have a result, the group hasn't been fully tested if (results.some(result => !result)) return undefined; - // Only show 'passed' if ALL tests have passed - const allPassed = results.every(result => result && result.status === 'passed'); + // Only show 'passed' if ALL tests have pass + const allPassed = results.every(result => result?.status === 'passed'); return allPassed ? 'passed' : 'failed'; }; @@ -378,6 +494,8 @@ export function useTestList(params: RefParams) { groupedTests, sortedGroupEntries, testResults, + showUnsavedModal, + pendingNavigation, getGroupResult, toggleExpanded, toggleGroupExpanded, @@ -393,5 +511,7 @@ export function useTestList(params: RefParams) { handleRenameGroup, handleTestNameEdit, handleTestNameBlur, + handleConfirmNavigation, + handleCancelNavigation, }; } From c205b858ca330af2954b48df9bb81bb13ee80fca Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Tue, 16 Sep 2025 17:58:02 -0700 Subject: [PATCH 06/34] more ux improvements and updating PR merge page --- .../TestResultsListItem/index.module.css | 54 ++++ .../TestResultsListItem/index.tsx | 72 ++++++ .../TestResultsForMergeList/index.module.css | 110 ++++++++ .../TestResultsForMergeList/index.tsx | 155 ++++++++++++ .../ForPulls/PullActions/Merge/Arrow.tsx | 3 +- .../PullActions/Merge/index.module.css | 71 +++++- .../ForPulls/PullActions/Merge/index.tsx | 236 +++++++----------- .../DatabasePage/ForTests/TestGroup.tsx | 7 +- .../DatabasePage/ForTests/TestItem.tsx | 5 +- .../DatabasePage/ForTests/TestList.tsx | 192 +++++++++++--- .../DatabasePage/ForTests/index.module.css | 83 +++++- .../DatabasePage/ForTests/queries.ts | 1 + .../DatabasePage/ForTests/useTestList.ts | 15 -- web/renderer/gen/graphql-types.tsx | 3 +- 14 files changed, 802 insertions(+), 205 deletions(-) create mode 100644 web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css create mode 100644 web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx create mode 100644 web/renderer/components/TestResultsForMergeList/index.module.css create mode 100644 web/renderer/components/TestResultsForMergeList/index.tsx diff --git a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css new file mode 100644 index 000000000..59b7d07ba --- /dev/null +++ b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css @@ -0,0 +1,54 @@ +.itemContainer { + @apply border-b border-stone-300 py-3 px-4 flex items-start gap-3 hover:bg-stone-50 transition-colors; +} + +.icon { + @apply flex-shrink-0 mt-0.5; + svg { + @apply w-4 h-4; + } +} + +.successIcon { + @apply text-green-500; +} + +.failureIcon { + @apply text-red-500; +} + +.pendingIcon { + @apply text-orange-500; +} + +.content { + @apply flex-1 min-w-0; +} + +.testTitle { + @apply flex flex-col gap-1; +} + +.testName { + @apply font-medium text-stone-700 text-sm; +} + +.testMessage { + @apply text-xs text-stone-600 break-words; +} + +.red { + @apply bg-red-50 border-red-300; +} + +.green { + @apply bg-green-50 border-green-300; +} + +.orange { + @apply bg-orange-50 border-orange-200; +} + +.linkContent { + @apply flex items-start gap-3 w-full no-underline; +} diff --git a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx new file mode 100644 index 000000000..3016bb87f --- /dev/null +++ b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx @@ -0,0 +1,72 @@ +import { + TestResult, +} from "@gen/graphql-types"; +import { RefParams } from "@lib/params"; +import cx from "classnames"; +import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; +import { FiX } from "@react-icons/all-files/fi/FiX"; +import { excerpt } from "@dolthub/web-utils"; +import css from "./index.module.css"; +import Link from "@components/links/Link"; +import { tests as testsUrl } from "@lib/urls" + +export type TestStatusColors = { + red: boolean; + orange: boolean; + green: boolean; +} + +type Props = { + test: TestResult; + params: RefParams; +}; + +function TestTitle({ test }: { test: TestResult }) { + return ( +
    + + {excerpt(test.testName, 50)} + + {test.message && ( + + {excerpt(test.message, 100)} + + )} +
    + ); +} + +function IconSwitch({ test }: { test: TestResult }) { + if (test.status === 'PASS') { + return ; + } + return ; +} + +export default function TestResultsListItem({ + test, + params, +}: Props) { + const isSuccess = test.status === 'PASS'; + const isFailure = !isSuccess; + + return ( +
  • + +
    + +
    +
    + +
    + +
  • + ); +} diff --git a/web/renderer/components/TestResultsForMergeList/index.module.css b/web/renderer/components/TestResultsForMergeList/index.module.css new file mode 100644 index 000000000..35a2d8486 --- /dev/null +++ b/web/renderer/components/TestResultsForMergeList/index.module.css @@ -0,0 +1,110 @@ +.outer { + @apply mx-auto max-w-3xl flex flex-col mb-4 mt-4; + + h3 { + @apply text-center mt-4 mb-4; + } + + @screen lg { + @apply flex-row; + } +} + +.container { + @apply w-full mb-0 rounded-xl min-w-[15rem]; + @screen lg { + @apply w-[calc(100%-4rem)] mx-3 mb-0; + } +} + +.top { + @apply bg-green-50 pl-4 pr-4 py-2 border border-green-500 flex justify-between items-start rounded-t-xl flex-wrap w-full; + + @screen lg { + @apply items-center pl-6; + } +} + + +.picContainer { + @apply w-7 h-7 mb-4 mr-1 text-white relative rounded-full bg-green-400 flex-shrink-0; + + @screen lg { + @apply mb-0 mt-3 ml-2; + } + svg { + @apply text-white mt-1 ml-1 w-5 h-5; + } +} + + +.redIcon { + @apply bg-red-300; +} + +.orangeIcon { + @apply bg-orange-300; +} + +.tests { + @apply border-x border-b rounded-b-xl bg-white border-green-500; + li:last-child { + @apply rounded-b-xl overflow-hidden; + } +} + +.green { + @apply text-green-500; +} + +.testsStatusIcon { + @apply inline-block mr-2 text-2xl; +} + +.red { + @apply text-red-400 flex border-red-400 bg-coral-50; +} + +.testsRed { + @apply border-red-400; +} + +.orange { + @apply text-orange-500 flex border-orange-300 bg-orange-50; +} + +.testsOrange { + @apply border-orange-300; +} + +.testsGreen { + @apply border-green-500; +} + + +.noTests { + @apply py-4 px-6 text-center text-stone-500 text-sm; +} + +.runButton { + @apply mx-2; + @screen lg { + @apply mx-0; + } +} + +.titleSection { + @apply flex items-center; +} + +.greenButton { + @apply bg-green-500 text-white border-green-500; +} + +.redButton { + @apply bg-red-500 text-white border-red-500; +} + +.orangeButton { + @apply bg-orange-500 text-white border-orange-500; +} diff --git a/web/renderer/components/TestResultsForMergeList/index.tsx b/web/renderer/components/TestResultsForMergeList/index.tsx new file mode 100644 index 000000000..0677e1e06 --- /dev/null +++ b/web/renderer/components/TestResultsForMergeList/index.tsx @@ -0,0 +1,155 @@ +import { + TestResult, useRunTestsLazyQuery, +} from "@gen/graphql-types"; +import { RefParams } from "@lib/params"; +import { Button } from "@dolthub/react-components"; +import cx from "classnames"; +import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; +import { FiX } from "@react-icons/all-files/fi/FiX"; +import { FiCircle } from "@react-icons/all-files/fi/FiCircle"; +import TestResultsListItem, { + TestStatusColors, +} from "./TestResultsListItem"; +import css from "./index.module.css"; +import { useCallback, useState } from "react"; +import { Arrow } from "@pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow"; + +type Props = { + params: RefParams; +}; + +function TestResultsTitle({ red, green, orange, onRunTests }: TestStatusColors & { onRunTests: () => void }) { + return ( + <> +
    + + Tests +
    + + + ); +} + +function IconSwitch({ + red, + green, + orange, + className, +}: TestStatusColors & { className?: string }) { + if (red) return ; + if (orange) return ; + if (green) return ; + return null; +} + +export default function TestResultsForMergeList({ params }: Props) { + const [testResults, setTestResults] = useState([]); + const [runTests] = useRunTestsLazyQuery(); + + const handleRunTests = useCallback(async () => { + try { + const result = await runTests({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + }, + }); + + setTestResults(result.data?.runTests.list ?? []); + + } catch { + setTestResults([]); + } + }, [runTests, params.databaseName, params.refName]); + + const { red, orange, green } = getTestStatusColors(testResults); + + return ( +
    + + + + +
    +
    + +
    +
    +
      + {testResults.length > 0 ? ( + testResults.map(test => ( + + )) + ) : ( +
    • + No test results available. Run tests to see results here. +
    • + )} +
    +
    +
    +
    + ); +} + +function getTestStatusColors(tests: TestResult[]): TestStatusColors { + if (tests.length === 0) { + return { + red: false, + green: false, + orange: true, + }; + } + + const failedTests = tests.filter(t => t.status !== 'PASS'); + + if (failedTests.length > 0) { + return { + red: true, + green: false, + orange: false, + }; + } + + return { + red: false, + green: true, + orange: false, + }; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx index 3c812e696..df8a0e370 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx @@ -1,13 +1,14 @@ import cx from "classnames"; import css from "./index.module.css"; -export function Arrow(props: { red?: boolean; green?: boolean }) { +export function Arrow(props: { red?: boolean; green?: boolean, orange?: boolean }) { return (
    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css index 4a15b7f84..6426fc565 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css @@ -1,3 +1,15 @@ +.wrapper { + @apply flex justify-between mt-4 flex-col-reverse; + @screen md { + @apply flex-row; + } +} + +.mergeDetails { + @screen md { + @apply w-9/12 mx-auto; + } +} .outer { @apply mx-auto max-w-3xl flex flex-col; @@ -6,14 +18,14 @@ } @screen lg { - @apply mt-6 flex-row; + @apply flex-row; } } .container { @apply w-full mb-4 rounded-xl min-w-[15rem]; @screen lg { - @apply mx-3 mb-0; + @apply w-[calc(100%-4rem)] mx-3 mb-0; } } @@ -140,6 +152,10 @@ @apply bg-green-50 border-green-500; } +.arrowOrange { + @apply bg-orange-50 border-orange-500; +} + .arrowLeft { @apply w-4 h-4 absolute rounded-sm rotate-45 border-l border-t -top-2 left-3; @screen lg { @@ -176,7 +192,7 @@ } .testActions { - @apply flex flex-col gap-1 items-center justify-center; + @apply flex flex-row gap-2 items-center justify-end; } .testButton { @@ -206,3 +222,52 @@ @apply mb-1 text-xs; } } + +.dropdown { + @apply relative; +} + +.dropdownButton { + @apply flex items-center gap-1.5; +} + +.chevron { + @apply w-4 h-4 transition-transform duration-200; +} + +.chevronOpen { + @apply rotate-180; +} + +.dropdownMenu { + @apply absolute right-0 top-full mt-2 bg-white border border-stone-300 rounded-lg shadow-xl z-50 min-w-[160px] py-1; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +.dropdownItem { + @apply w-full text-left px-4 py-2.5 text-sm text-stone-700 hover:bg-stone-50 hover:text-stone-700 border-none bg-transparent cursor-pointer transition-colors duration-150 font-medium; + + &:first-child { + @apply rounded-t-lg; + } + + &:last-child { + @apply rounded-b-lg; + } + + &:hover { + @apply bg-indigo-50 text-indigo-700; + } +} + +.dropdownLink { + @apply block w-full text-left px-4 py-2.5 text-sm text-stone-700 hover:bg-indigo-50 hover:text-indigo-700 no-underline transition-colors duration-150 font-medium; + + &:first-child { + @apply rounded-t-lg; + } + + &:last-child { + @apply rounded-b-lg; + } +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx index 315363db5..ed2779ae7 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx @@ -6,10 +6,9 @@ import { ApolloErrorType } from "@lib/errors/types"; import { PullDiffParams } from "@lib/params"; import { tests } from "@lib/urls"; import { FiGitPullRequest } from "@react-icons/all-files/fi/FiGitPullRequest"; -import { FiMoreHorizontal } from "@react-icons/all-files/fi/FiMoreHorizontal"; +import { FiChevronDown } from "@react-icons/all-files/fi/FiChevronDown"; import cx from "classnames"; -import Link from "next/link"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useCallback, useRef, useEffect } from "react"; import { Arrow } from "./Arrow"; import ErrorsWithDirections from "./ErrorsWithDirections"; import MergeConflictsDirections from "./ErrorsWithDirections/MergeConflictsDirections"; @@ -18,6 +17,9 @@ import MergeMessageTitle from "./MergeMessageTitle"; import ResolveModal from "./ResolveModal"; import css from "./index.module.css"; import useMergeButton, { MergeButtonState } from "./useMergeButton"; +import Link from "@components/links/Link"; +import TestResultsListItem from "@components/TestResultsForMergeList/TestResultsListItem"; +import TestResultsForMergeList from "@components/TestResultsForMergeList"; type Props = { params: PullDiffParams; @@ -41,67 +43,70 @@ export default function Merge(props: Props) { const red = hasConflicts; return ( -
    - - - - -
    -
    - - - {mergeState.err && ( - setState({ showDirections: s })} - /> - )} -
    - - - -
    - setState({ addAuthor: a })} - userHeaders={userHeaders} - className={css.userCheckbox} - kind="merge commit" - /> - - {hasConflicts && ( - +
    + +
    + + + + +
    +
    + + + {mergeState.err && ( + setState({ showDirections: s })} + /> + )} +
    + +
    + setState({ addAuthor: a })} + userHeaders={userHeaders} + className={css.userCheckbox} + kind="merge commit" + /> + - )} - - View{" "} - - setState({ showDirections: !state.showDirections }) - } - > - manual merge instructions - - . - - {state.showDirections && } + {hasConflicts && ( + + )} + + View{" "} + + setState({ showDirections: !state.showDirections }) + } + > + manual merge instructions + + . + + {state.showDirections && } +
    +
    ); } @@ -158,95 +163,42 @@ type TestRunnerProps = { }; function TestRunner({ params }: TestRunnerProps) { - const [testResults, setTestResults] = useState<{ status: 'passed' | 'failed' | null, failedTests?: string[] }>({ status: null }); - const { data: testsData } = useTestListQuery({ - variables: { - databaseName: params.databaseName, - refName: params.refName, - }, - }); - const [runTests] = useRunTestsLazyQuery(); - const hasAutoRun = useRef(false); - - const handleRunTests = useCallback(async () => { - if (!testsData?.tests.list.length) return; - - try { - const result = await runTests({ - variables: { - databaseName: params.databaseName, - refName: params.refName, - }, - }); - - const testResults = result.data?.runTests.list ?? []; - const failedTests = testResults.filter(t => t.status !== 'PASS'); - - if (failedTests.length === 0) { - setTestResults({ status: 'passed' }); - } else { - setTestResults({ - status: 'failed', - failedTests: failedTests.map(t => `${t.testName}: ${t.message}`) - }); - } - } catch (error) { - setTestResults({ - status: 'failed', - failedTests: ['Error running tests: ' + (error as Error).message] - }); - } - }, [testsData?.tests.list.length, runTests, params.databaseName, params.refName]); + return ( + + ) +} - // Auto-run tests when testsData is available - useEffect(() => { - if (testsData?.tests.list.length && !hasAutoRun.current) { - hasAutoRun.current = true; - void handleRunTests(); - } - }, [testsData?.tests.list.length, handleRunTests]); +type TestDropdownProps = { + onRunTests: () => Promise; + testsUrl: any; +}; - if (!testsData?.tests.list.length) { - return null; // No tests available - } +function TestDropdown({ onRunTests, testsUrl }: TestDropdownProps) { + const [isOpen, setIsOpen] = useState(false); - const testsUrl = tests({ databaseName: params.databaseName, refName: params.refName }).withQuery({ runTests: 'true' }); + const handleRunTests = () => { + setIsOpen(false); + void onRunTests(); + }; return ( -
    -
    -
    - {testResults.status === 'passed' && ( - All tests passed ({testsData.tests.list.length} tests) - )} - {testResults.status === 'failed' && ( -
    - Some tests failed: -
      - {testResults.failedTests?.map((error, idx) => ( -
    • {error}
    • - ))} -
    -
    - )} - {testResults.status === null && ( - Running tests to validate changes... - )} -
    -
    - - +
    + + {isOpen && ( +
    + + + View Details
    -
    + )}
    ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx index be5374081..ccbc7a4e2 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx @@ -5,6 +5,7 @@ import { FaCheck } from "@react-icons/all-files/fa/FaCheck"; import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; import { Button } from "@dolthub/react-components"; import { useState, KeyboardEvent, ChangeEvent, MouseEvent } from "react"; +import cx from "classnames"; import css from "./index.module.css"; import ConfirmationModal from "./ConfirmationModal"; @@ -13,14 +14,14 @@ type Props = { isExpanded: boolean; onToggle: () => void; testCount: number; - groupColor: string; + className?: string; groupResult?: 'passed' | 'failed'; onRunGroup: () => void; onDeleteGroup: () => void; onRenameGroup?: (oldName: string, newName: string) => void; }; -export default function TestGroup({ group, isExpanded, onToggle, testCount, groupColor, groupResult, onRunGroup, onDeleteGroup, onRenameGroup }: Props) { +export default function TestGroup({ group, isExpanded, onToggle, testCount, className, groupResult, onRunGroup, onDeleteGroup, onRenameGroup }: Props) { const groupName = group || "No Group"; const [localGroupName, setLocalGroupName] = useState(groupName); const [isEditing, setIsEditing] = useState(false); @@ -74,7 +75,7 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, grou }; return ( -
    +
    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx index eacca33ea..6dc18c045 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem.tsx @@ -4,6 +4,7 @@ import { FaPlay } from "@react-icons/all-files/fa/FaPlay"; import { FaTrash } from "@react-icons/all-files/fa/FaTrash"; import { FaCheck } from "@react-icons/all-files/fa/FaCheck"; import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; +import cx from "classnames"; import css from "./index.module.css"; import QueryEditor from "./QueryEditor"; import { MouseEvent, useState } from "react"; @@ -16,6 +17,7 @@ type Props = { isExpanded: boolean; editingName: string | undefined; testResult?: {status: 'passed' | 'failed', error?: string}; + className?: string; onToggleExpanded: () => void; onUpdateTest: (field: keyof Test, value: string) => void; onNameEdit: (name: string) => void; @@ -30,6 +32,7 @@ export default function TestItem({ isExpanded, editingName, testResult, + className, onToggleExpanded, onUpdateTest, onNameEdit, @@ -54,7 +57,7 @@ export default function TestItem({ }; return ( -
  • +
  • diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx index a414ce7df..814f26db4 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx @@ -1,6 +1,7 @@ import HideForNoWritesWrapper from "@components/util/HideForNoWritesWrapper"; -import { Button } from "@dolthub/react-components"; -import { useState } from "react"; +import { Button, Popup } from "@dolthub/react-components"; +import { useState, useEffect, useCallback } from "react"; +import cx from "classnames"; import css from "./index.module.css"; import NewGroupModal from "./NewGroupModal"; import TestGroup from "./TestGroup"; @@ -8,6 +9,9 @@ import TestItem from "./TestItem"; import ConfirmationModal from "./ConfirmationModal"; import { useTestList } from "./useTestList"; import { RefParams } from "@lib/params"; +import { FaCaretDown } from "@react-icons/all-files/fa/FaCaretDown"; +import { FaCaretUp } from "@react-icons/all-files/fa/FaCaretUp"; +import { FiPlus } from "@react-icons/all-files/fi/FiPlus"; type Props = { params: RefParams; @@ -16,6 +20,7 @@ type Props = { export default function TestList({ params }: Props) { const [showNewGroupModal, setShowNewGroupModal] = useState(false); const [newGroupName, setNewGroupName] = useState(""); + const [hasHandledHash, setHasHandledHash] = useState(false); const { expandedItems, @@ -27,7 +32,6 @@ export default function TestList({ params }: Props) { sortedGroupEntries, testResults, showUnsavedModal, - pendingNavigation, getGroupResult, toggleExpanded, toggleGroupExpanded, @@ -47,8 +51,44 @@ export default function TestList({ params }: Props) { handleCancelNavigation, } = useTestList(params); - // Debug: Log testResults to see if they're being received in the UI - console.log('DEBUG TestList: testResults state:', testResults); + // Handle URL hash navigation to specific tests + const handleHashNavigation = useCallback(() => { + const hash = window.location.hash.slice(1); + if (!hash || tests.length === 0 || hasHandledHash) return; + + const decodedHash = decodeURIComponent(hash); + const targetTest = tests.find(test => test.testName === decodedHash); + if (!targetTest) return; + + const containingGroup = Object.entries(groupedTests).find( + ([, groupTests]) => groupTests.some(test => test.testName === decodedHash), + )?.[0]; + + if (containingGroup && containingGroup !== "" && !expandedGroups.has(containingGroup)) { + toggleGroupExpanded(containingGroup); + } + + if (!expandedItems.has(decodedHash)) { + toggleExpanded(decodedHash); + } + + setHasHandledHash(true); + + // scroll to the test after a short delay to ensure DOM is updated + setTimeout(() => { + const testElement = document.querySelector(`[data-test-name="${decodedHash}"]`); + if (testElement) { + testElement.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + } + }, 100); + }, [tests, groupedTests, expandedGroups, expandedItems, toggleGroupExpanded, toggleExpanded, hasHandledHash]); + + useEffect(() => { + handleHashNavigation(); + }, [handleHashNavigation]); const onCreateGroup = () => { if (handleCreateGroup(newGroupName, groupedTests)) { @@ -59,36 +99,100 @@ export default function TestList({ params }: Props) { const uniqueGroups = sortedGroupEntries.map(entry => entry[0]).filter(group => group !== "") + const getGroupStatusColors = (groupTests: any[]) => { + const groupTestResults = groupTests.map(test => testResults[test.testName]).filter(Boolean); + + if (groupTestResults.length === 0) { + return { red: false, green: false, orange: true }; + } + + const hasFailures = groupTestResults.some(result => result && result.status === 'failed'); + if (hasFailures) { + return { red: true, green: false, orange: false }; + } + + return { red: false, green: true, orange: false }; + }; + + const getTestStatusColors = (testName: string) => { + const testResult = testResults[testName]; + + if (!testResult) { + return { red: false, green: false, orange: true }; + } + + if (testResult.status === 'failed') { + return { red: true, green: false, orange: false }; + } + + return { red: false, green: true, orange: false }; + }; + + const CreateDropdown = () => ( + ( + + )} + > +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    + ); + return (

    Tests

    -
    - - <> - - +
    +
    + + + +
    + +
    + - - - {tests.length > 0 && ( - - )} + + {tests.length > 0 && ( + + )} +
    @@ -106,7 +210,7 @@ export default function TestList({ params }: Props) { .filter(([groupName]) => groupName !== "") .map(([groupName, groupTests]) => { const isGroupExpanded = expandedGroups.has(groupName); - const groupColor = "#f59e0b"; + const groupStatusColors = getGroupStatusColors(groupTests); return (
    @@ -115,15 +219,21 @@ export default function TestList({ params }: Props) { isExpanded={isGroupExpanded} onToggle={() => toggleGroupExpanded(groupName)} testCount={groupTests.length} - groupColor={groupColor} + className={cx({ + [css.greenGroup]: groupStatusColors.green, + [css.redGroup]: groupStatusColors.red, + [css.orangeGroup]: groupStatusColors.orange, + })} groupResult={getGroupResult(groupName)} onRunGroup={async () => await handleRunGroup(groupName)} - onDeleteGroup={() => handleDeleteGroup(groupName)} + onDeleteGroup={async () => handleDeleteGroup(groupName)} onRenameGroup={handleRenameGroup} /> {isGroupExpanded && (
      - {groupTests.map((test) => ( + {groupTests.map((test) => { + const testStatusColors = getTestStatusColors(test.testName); + return ( toggleExpanded(test.testName)} onUpdateTest={(field, value) => updateTest(test.testName, field, value)} onNameEdit={(name) => handleTestNameEdit(test.testName, name)} onNameBlur={() => handleTestNameBlur(test.testName)} onRunTest={async () => await handleRunTest(test.testName)} - onDeleteTest={() => handleDeleteTest(test.testName)} + onDeleteTest={async () => await handleDeleteTest(test.testName)} /> - ))} + ); + })}
    )}
    @@ -149,7 +265,9 @@ export default function TestList({ params }: Props) {
    Ungrouped
      - {groupedTests[""].map((test) => ( + {groupedTests[""].map((test) => { + const testStatusColors = getTestStatusColors(test.testName); + return ( toggleExpanded(test.testName)} onUpdateTest={(field, value) => updateTest(test.testName, field, value)} onNameEdit={(name) => handleTestNameEdit(test.testName, name)} onNameBlur={() => handleTestNameBlur(test.testName)} onRunTest={async () => await handleRunTest(test.testName)} - onDeleteTest={() => handleDeleteTest(test.testName)} + onDeleteTest={async () => await handleDeleteTest(test.testName)} /> - ))} + ); + })}
    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css index bed35857c..35c85372a 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css @@ -23,7 +23,7 @@ } .item { - @apply border border-stone-50 rounded-lg my-3 py-4 px-6 text-sm relative; + @apply border-2 border-stone-50 rounded-lg my-3 py-4 px-6 text-sm relative; } .header { @@ -87,7 +87,7 @@ } .expandedContent { - @apply mt-4 p-4 bg-stone-50 rounded border-l-4 border-sky-400; + @apply mt-4 p-4 bg-stone-50 rounded; } .fieldGroup { @@ -251,14 +251,62 @@ @apply opacity-100; } -.createButtons { - @apply flex gap-2 mb-6; +.actionArea { + @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6; } -.createButtons button { +.createActions { + @apply flex gap-2; +} + +.createActions button { @apply px-3 py-1.5 text-sm; } +.primaryActions { + @apply flex gap-3; +} + +.primaryActions button { + @apply px-4 py-2 text-sm font-medium; + height: 38px; + min-width: 120px; +} + +/* Create Dropdown Styles */ +.createButton { + @apply flex items-center justify-center px-4 py-2 text-sm font-medium bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; + height: 38px; + min-width: 120px; +} + +.createButton:focus { + @apply outline-none ring-2 ring-sky-500 ring-offset-2; +} + +.plus { + @apply mr-1.5; +} + +.caret { + @apply ml-1.5; +} + +.createPopup { + @apply bg-white border border-stone-300 rounded-md shadow-lg py-1 min-w-[140px]; +} + +.createPopupItem { + @apply m-0 p-0; +} + +.createPopupItem button { + @apply w-full text-left px-4 py-2 text-sm text-stone-700 hover:bg-stone-50 focus:bg-stone-50 transition-colors duration-150; + border: none; + background: transparent; + outline: none; +} + .saveActions { @apply flex gap-2 mt-4 pt-4 border-t border-stone-300; } @@ -335,3 +383,28 @@ .ungroupedTests { @apply mb-6; } + +/* Dynamic test status colors */ +.greenGroup { + @apply border-l-green-400 text-green-400; +} + +.redGroup { + @apply border-l-red-400 text-red-400; +} + +.orangeGroup { + @apply text-orange-500; +} + +/*.greenTest {*/ +/* @apply bg-green-50;*/ +/*}*/ + +/*.redTest {*/ +/* @apply bg-red-50;*/ +/*}*/ + +/*.orangeTest {*/ +/* @apply bg-orange-50;*/ +/*}*/ diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts index 72694b7d6..f3acdb55f 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts @@ -36,6 +36,7 @@ export const RUN_TESTS = gql` list { testName testGroupName + query status message } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts index 53a47e38d..bef9ec6c3 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/useTestList.ts @@ -27,7 +27,6 @@ export function useTestList(params: RefParams) { const [pendingNavigation, setPendingNavigation] = useState(null); const autoRunExecutedRef = useRef(false); - // Update tests when GraphQL data loads useEffect(() => { if (data?.tests.list) { const initialTests = data.tests.list.map(({ __typename, ...test }) => test); @@ -42,7 +41,6 @@ export function useTestList(params: RefParams) { setPendingNavigation(null); setHasUnsavedChanges(false); // Clear unsaved changes to allow navigation - // Use setTimeout to ensure state updates are processed setTimeout(async () => { await router.push(url); }); @@ -54,7 +52,6 @@ export function useTestList(params: RefParams) { setPendingNavigation(null); }; - // Warn about unsaved changes when navigating away useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (hasUnsavedChanges) { @@ -73,10 +70,8 @@ export function useTestList(params: RefParams) { } }; - // Handle browser navigation (refresh, close tab, etc.) window.addEventListener('beforeunload', handleBeforeUnload); - // Handle Next.js client-side navigation router.events.on('routeChangeStart', handleRouteChangeStart); return () => { @@ -85,7 +80,6 @@ export function useTestList(params: RefParams) { }; }, [hasUnsavedChanges, router]); - // Auto-run tests if runTests query parameter is present useEffect(() => { const shouldRunTests = router.query.runTests === 'true'; console.log('DEBUG: Auto-run check:', { @@ -96,14 +90,12 @@ export function useTestList(params: RefParams) { pathname: router.pathname }); - // Reset the flag when runTests param is present to allow re-running if (shouldRunTests) { autoRunExecutedRef.current = false; } if (shouldRunTests && tests.length > 0 && !autoRunExecutedRef.current) { console.log('DEBUG: Starting auto-run of tests'); - // Run all tests automatically const runAllTests = async () => { autoRunExecutedRef.current = true; // Prevent re-running console.log('DEBUG: Executing runTests GraphQL mutation'); @@ -143,7 +135,6 @@ export function useTestList(params: RefParams) { void runAllTests(); } - // Keep the runTests query parameter to preserve the auto-run behavior }, [router.query.runTests, tests.length, router, runTests, params.databaseName, params.refName, data?.tests.list]); const [saveTestsMutation] = useSaveTestsMutation({ @@ -340,7 +331,6 @@ export function useTestList(params: RefParams) { }; const handleCreateTest = (groupName?: string) => { - // Generate a unique test name const baseTestName = "New Test"; const existingNames = tests.map(t => t.testName); let uniqueTestName = baseTestName; @@ -367,7 +357,6 @@ export function useTestList(params: RefParams) { }); setHasUnsavedChanges(true); - // Remove from empty groups if test is added to it if (groupName && emptyGroups.has(groupName)) { setEmptyGroups(prev => { const newSet = new Set(prev); @@ -380,7 +369,6 @@ export function useTestList(params: RefParams) { setExpandedGroups(prev => new Set([...prev, groupName])); } - // Scroll to new test setTimeout(() => { const testElement = document.querySelector(`[data-test-name="${newTest.testName}"]`); testElement?.scrollIntoView({ behavior: "smooth", block: "center" }); @@ -439,7 +427,6 @@ export function useTestList(params: RefParams) { groups[groupName].push(test); }); - // Add empty groups emptyGroups.forEach(groupName => { groups[groupName] ??= []; }); @@ -477,10 +464,8 @@ export function useTestList(params: RefParams) { const results = groupTests.map(test => testResults[test.testName]); - // If any test doesn't have a result, the group hasn't been fully tested if (results.some(result => !result)) return undefined; - // Only show 'passed' if ALL tests have pass const allPassed = results.every(result => result?.status === 'passed'); return allPassed ? 'passed' : 'failed'; }; diff --git a/web/renderer/gen/graphql-types.tsx b/web/renderer/gen/graphql-types.tsx index ed0cb2edc..ebd7a3bb5 100644 --- a/web/renderer/gen/graphql-types.tsx +++ b/web/renderer/gen/graphql-types.tsx @@ -1602,7 +1602,7 @@ export type RunTestsQueryVariables = Exact<{ }>; -export type RunTestsQuery = { __typename?: 'Query', runTests: { __typename?: 'TestResultList', list: Array<{ __typename?: 'TestResult', testName: string, testGroupName?: string | null, status: string, message: string }> } }; +export type RunTestsQuery = { __typename?: 'Query', runTests: { __typename?: 'TestResultList', list: Array<{ __typename?: 'TestResult', testName: string, testGroupName?: string | null, query: string, status: string, message: string }> } }; export type LoadDataMutationVariables = Exact<{ databaseName: Scalars['String']['input']; @@ -4657,6 +4657,7 @@ export const RunTestsDocument = gql` list { testName testGroupName + query status message } From e0b9ab3960fe26e47ddf44784a2cfe2a23391cde Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 17 Sep 2025 11:28:09 -0700 Subject: [PATCH 07/34] file structure refactor and cleanup --- .../TestResultsListItem/index.tsx | 61 ++- .../TestResultsForMergeList/index.module.css | 73 ++-- .../TestResultsForMergeList/index.tsx | 50 ++- .../ForPulls/PullActions/Merge/Arrow.tsx | 6 +- .../PullActions/Merge/index.module.css | 38 +- .../ForPulls/PullActions/Merge/index.tsx | 167 +++---- .../index.module.css} | 0 .../index.tsx} | 18 +- .../ForTests/NewGroupModal/index.module.css | 20 + .../index.tsx} | 18 +- .../ForTests/QueryEditor/index.module.css | 4 + .../index.tsx} | 5 +- .../ForTests/TestGroup/index.module.css | 97 +++++ .../{TestGroup.tsx => TestGroup/index.tsx} | 58 ++- .../ForTests/TestItem/index.module.css | 104 +++++ .../{TestItem.tsx => TestItem/index.tsx} | 84 ++-- .../ForTests/TestList/index.module.css | 248 +++++++++++ .../{TestList.tsx => TestList/index.tsx} | 215 +++++---- .../ForTests/{ => TestList}/useTestList.ts | 278 ++++++------ .../DatabasePage/ForTests/index.module.css | 410 ------------------ .../DatabasePage/ForTests/queries.ts | 23 +- .../[databaseName]/tests/[refName]/index.tsx | 4 +- 22 files changed, 1051 insertions(+), 930 deletions(-) rename web/renderer/components/pageComponents/DatabasePage/ForTests/{ConfirmationModal.module.css => ConfirmationModal/index.module.css} (100%) rename web/renderer/components/pageComponents/DatabasePage/ForTests/{ConfirmationModal.tsx => ConfirmationModal/index.tsx} (83%) create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css rename web/renderer/components/pageComponents/DatabasePage/ForTests/{NewGroupModal.tsx => NewGroupModal/index.tsx} (60%) create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css rename web/renderer/components/pageComponents/DatabasePage/ForTests/{QueryEditor.tsx => QueryEditor/index.tsx} (89%) create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css rename web/renderer/components/pageComponents/DatabasePage/ForTests/{TestGroup.tsx => TestGroup/index.tsx} (76%) create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css rename web/renderer/components/pageComponents/DatabasePage/ForTests/{TestItem.tsx => TestItem/index.tsx} (71%) create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css rename web/renderer/components/pageComponents/DatabasePage/ForTests/{TestList.tsx => TestList/index.tsx} (56%) rename web/renderer/components/pageComponents/DatabasePage/ForTests/{ => TestList}/useTestList.ts (68%) delete mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css diff --git a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx index 3016bb87f..4e0bc892c 100644 --- a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx +++ b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx @@ -1,6 +1,4 @@ -import { - TestResult, -} from "@gen/graphql-types"; +import { TestResult } from "@gen/graphql-types"; import { RefParams } from "@lib/params"; import cx from "classnames"; import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; @@ -8,13 +6,13 @@ import { FiX } from "@react-icons/all-files/fi/FiX"; import { excerpt } from "@dolthub/web-utils"; import css from "./index.module.css"; import Link from "@components/links/Link"; -import { tests as testsUrl } from "@lib/urls" +import { tests as testsUrl } from "@lib/urls"; export type TestStatusColors = { red: boolean; orange: boolean; green: boolean; -} +}; type Props = { test: TestResult; @@ -24,49 +22,44 @@ type Props = { function TestTitle({ test }: { test: TestResult }) { return (
    - - {excerpt(test.testName, 50)} - + {excerpt(test.testName, 50)} {test.message && ( - - {excerpt(test.message, 100)} - + {excerpt(test.message, 100)} )}
    ); } function IconSwitch({ test }: { test: TestResult }) { - if (test.status === 'PASS') { + if (test.status === "PASS") { return ; } return ; } -export default function TestResultsListItem({ - test, - params, -}: Props) { - const isSuccess = test.status === 'PASS'; +export default function TestResultsListItem({ test, params }: Props) { + const isSuccess = test.status === "PASS"; const isFailure = !isSuccess; return ( -
  • - -
    - -
    -
    - -
    - -
  • +
  • + +
    + +
    +
    + +
    + +
  • ); } diff --git a/web/renderer/components/TestResultsForMergeList/index.module.css b/web/renderer/components/TestResultsForMergeList/index.module.css index 35a2d8486..4f7cbed12 100644 --- a/web/renderer/components/TestResultsForMergeList/index.module.css +++ b/web/renderer/components/TestResultsForMergeList/index.module.css @@ -1,87 +1,84 @@ .outer { - @apply mx-auto max-w-3xl flex flex-col mb-4 mt-4; + @apply mx-auto max-w-3xl flex flex-col mb-4 mt-4; - h3 { - @apply text-center mt-4 mb-4; - } + h3 { + @apply text-center mt-4 mb-4; + } - @screen lg { - @apply flex-row; - } + @screen lg { + @apply flex-row; + } } .container { - @apply w-full mb-0 rounded-xl min-w-[15rem]; - @screen lg { - @apply w-[calc(100%-4rem)] mx-3 mb-0; - } + @apply w-full mb-0 rounded-xl min-w-[15rem]; + @screen lg { + @apply w-[calc(100%-4rem)] mx-3 mb-0; + } } .top { - @apply bg-green-50 pl-4 pr-4 py-2 border border-green-500 flex justify-between items-start rounded-t-xl flex-wrap w-full; + @apply bg-green-50 pl-4 pr-4 py-2 border border-green-500 flex justify-between items-start rounded-t-xl flex-wrap w-full; - @screen lg { - @apply items-center pl-6; - } + @screen lg { + @apply items-center pl-6; + } } - .picContainer { - @apply w-7 h-7 mb-4 mr-1 text-white relative rounded-full bg-green-400 flex-shrink-0; + @apply w-7 h-7 mb-4 mr-1 text-white relative rounded-full bg-green-400 flex-shrink-0; - @screen lg { - @apply mb-0 mt-3 ml-2; - } - svg { - @apply text-white mt-1 ml-1 w-5 h-5; - } + @screen lg { + @apply mb-0 mt-3 ml-2; + } + svg { + @apply text-white mt-1 ml-1 w-5 h-5; + } } - .redIcon { - @apply bg-red-300; + @apply bg-red-300; } .orangeIcon { - @apply bg-orange-300; + @apply bg-orange-300; } .tests { - @apply border-x border-b rounded-b-xl bg-white border-green-500; - li:last-child { - @apply rounded-b-xl overflow-hidden; - } + @apply border-x border-b rounded-b-xl bg-white border-green-500; + li:last-child { + @apply rounded-b-xl overflow-hidden; + } } .green { - @apply text-green-500; + @apply text-green-500; } .testsStatusIcon { - @apply inline-block mr-2 text-2xl; + @apply inline-block mr-2 text-2xl; } .red { - @apply text-red-400 flex border-red-400 bg-coral-50; + @apply text-red-400 flex border-red-400 bg-coral-50; } .testsRed { - @apply border-red-400; + @apply border-red-400; } .orange { - @apply text-orange-500 flex border-orange-300 bg-orange-50; + @apply text-orange-500 flex border-orange-300 bg-orange-50; } .testsOrange { - @apply border-orange-300; + @apply border-orange-300; } .testsGreen { - @apply border-green-500; + @apply border-green-500; } - .noTests { @apply py-4 px-6 text-center text-stone-500 text-sm; } diff --git a/web/renderer/components/TestResultsForMergeList/index.tsx b/web/renderer/components/TestResultsForMergeList/index.tsx index 0677e1e06..40435386c 100644 --- a/web/renderer/components/TestResultsForMergeList/index.tsx +++ b/web/renderer/components/TestResultsForMergeList/index.tsx @@ -1,15 +1,11 @@ -import { - TestResult, useRunTestsLazyQuery, -} from "@gen/graphql-types"; +import { TestResult, useRunTestsLazyQuery, useTestListQuery } from "@gen/graphql-types"; import { RefParams } from "@lib/params"; import { Button } from "@dolthub/react-components"; import cx from "classnames"; import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; import { FiX } from "@react-icons/all-files/fi/FiX"; import { FiCircle } from "@react-icons/all-files/fi/FiCircle"; -import TestResultsListItem, { - TestStatusColors, -} from "./TestResultsListItem"; +import TestResultsListItem, { TestStatusColors } from "./TestResultsListItem"; import css from "./index.module.css"; import { useCallback, useState } from "react"; import { Arrow } from "@pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow"; @@ -18,7 +14,12 @@ type Props = { params: RefParams; }; -function TestResultsTitle({ red, green, orange, onRunTests }: TestStatusColors & { onRunTests: () => void }) { +function TestResultsTitle({ + red, + green, + orange, + onRunTests, +}: TestStatusColors & { onRunTests: () => void }) { return ( <>
    @@ -34,7 +35,7 @@ function TestResultsTitle({ red, green, orange, onRunTests }: TestStatusColors & className={cx(css.runButton, { [css.greenButton]: green, [css.redButton]: red, - [css.orangeButton]: orange + [css.orangeButton]: orange, })} onClick={onRunTests} > @@ -58,8 +59,13 @@ function IconSwitch({ export default function TestResultsForMergeList({ params }: Props) { const [testResults, setTestResults] = useState([]); - const [runTests] = useRunTestsLazyQuery(); - + const [runTests] = useRunTestsLazyQuery(); + const { data } = useTestListQuery({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + }, + }); const handleRunTests = useCallback(async () => { try { const result = await runTests({ @@ -70,12 +76,15 @@ export default function TestResultsForMergeList({ params }: Props) { }); setTestResults(result.data?.runTests.list ?? []); - } catch { setTestResults([]); } }, [runTests, params.databaseName, params.refName]); + if (!data?.tests.list || data.tests.list.length === 0) { + return null; + } + const { red, orange, green } = getTestStatusColors(testResults); return ( @@ -98,7 +107,12 @@ export default function TestResultsForMergeList({ params }: Props) { [css.orange]: orange, })} > - +
    - No test results available. Run tests to see results here. + + No test results available. Run tests to see results here. + )} @@ -136,9 +152,9 @@ function getTestStatusColors(tests: TestResult[]): TestStatusColors { orange: true, }; } - - const failedTests = tests.filter(t => t.status !== 'PASS'); - + + const failedTests = tests.filter(t => t.status !== "PASS"); + if (failedTests.length > 0) { return { red: true, @@ -146,7 +162,7 @@ function getTestStatusColors(tests: TestResult[]): TestStatusColors { orange: false, }; } - + return { red: false, green: true, diff --git a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx index df8a0e370..20d002d3d 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow.tsx @@ -1,7 +1,11 @@ import cx from "classnames"; import css from "./index.module.css"; -export function Arrow(props: { red?: boolean; green?: boolean, orange?: boolean }) { +export function Arrow(props: { + red?: boolean; + green?: boolean; + orange?: boolean; +}) { return (
    - -
    - - - - -
    -
    - - - {mergeState.err && ( - setState({ showDirections: s })} + +
    + + + + +
    +
    + + - )} -
    + {mergeState.err && ( + setState({ showDirections: s })} + /> + )} +
    -
    - setState({ addAuthor: a })} - userHeaders={userHeaders} - className={css.userCheckbox} - kind="merge commit" - /> - - {hasConflicts && ( - + setState({ addAuthor: a })} + userHeaders={userHeaders} + className={css.userCheckbox} + kind="merge commit" /> - )} - - View{" "} - - setState({ showDirections: !state.showDirections }) - } - > - manual merge instructions - - . - - {state.showDirections && } + + {hasConflicts && ( + + )} + + View{" "} + + setState({ showDirections: !state.showDirections }) + } + > + manual merge instructions + + . + + {state.showDirections && } +
    -
    ); } @@ -157,48 +155,3 @@ function MergeButton(props: MergeButtonProps) {
    ); } - -type TestRunnerProps = { - params: PullDiffParams; -}; - -function TestRunner({ params }: TestRunnerProps) { - return ( - - ) -} - -type TestDropdownProps = { - onRunTests: () => Promise; - testsUrl: any; -}; - -function TestDropdown({ onRunTests, testsUrl }: TestDropdownProps) { - const [isOpen, setIsOpen] = useState(false); - - const handleRunTests = () => { - setIsOpen(false); - void onRunTests(); - }; - - return ( -
    - - {isOpen && ( -
    - - - View Details - -
    - )} -
    - ); -} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css similarity index 100% rename from web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal.module.css rename to web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx similarity index 83% rename from web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal.tsx rename to web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx index 1741c96e0..400dac4c4 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx @@ -1,7 +1,7 @@ import { Button } from "@dolthub/react-components"; import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; import { FaExclamationTriangle } from "@react-icons/all-files/fa/FaExclamationTriangle"; -import css from "./ConfirmationModal.module.css"; +import css from "./index.module.css"; type Props = { isOpen: boolean; @@ -28,27 +28,29 @@ export default function ConfirmationModal({ return (
    -
    e.stopPropagation()}> +
    e.stopPropagation()}>
    - {destructive && } + {destructive && ( + + )}

    {title}

    - +

    {message}

    - +
    -
    ); -} \ No newline at end of file +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css new file mode 100644 index 000000000..f62a9ec9f --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css @@ -0,0 +1,20 @@ +/* NewGroupModal Component Styles */ +.overlay { + @apply fixed inset-0 bg-stone-700 bg-opacity-50 flex items-center justify-center z-50; +} + +.modal { + @apply bg-white rounded-lg p-6 w-96 border border-stone-300 shadow-lg; +} + +.title { + @apply text-lg font-semibold mb-4; +} + +.fieldInput { + @apply w-full p-2 border border-stone-100 rounded text-sm; +} + +.buttonGroup { + @apply flex gap-2 mt-4; +} \ No newline at end of file diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.tsx similarity index 60% rename from web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx rename to web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.tsx index 0e591e666..4af7f62d1 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.tsx @@ -14,7 +14,7 @@ export default function NewGroupModal({ groupName, onGroupNameChange, onCreateGroup, - onClose + onClose, }: Props) { if (!isOpen) return null; @@ -24,24 +24,22 @@ export default function NewGroupModal({ }; return ( -
    -
    -

    Create New Test Group

    +
    +
    +

    Create New Test Group

    onGroupNameChange(e.target.value)} + onChange={e => onGroupNameChange(e.target.value)} placeholder="Enter group name..." - onKeyDown={(e) => e.key === 'Enter' && onCreateGroup()} + onKeyDown={e => e.key === "Enter" && onCreateGroup()} /> -
    +
    - +
    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css new file mode 100644 index 000000000..ae332f6b4 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css @@ -0,0 +1,4 @@ +/* QueryEditor Component Styles */ +.container { + @apply bg-storm-600 rounded-sm border border-storm-500 overflow-hidden p-2; +} \ No newline at end of file diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.tsx similarity index 89% rename from web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor.tsx rename to web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.tsx index d01ef3a92..4cfb097ce 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.tsx @@ -1,4 +1,5 @@ import dynamic from "next/dynamic"; +import css from "./index.module.css"; const AceEditor = dynamic(async () => import("@components/AceEditor"), { ssr: false, @@ -12,7 +13,7 @@ type Props = { export default function QueryEditor({ value, onChange, placeholder }: Props) { return ( -
    +
    ); -} \ No newline at end of file +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css new file mode 100644 index 000000000..8b5c28773 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css @@ -0,0 +1,97 @@ +.groupHeader { + @apply border-l-4 bg-stone-50 border-stone-300 rounded-r mb-4 cursor-pointer; + transition: all 0.2s ease; +} + +.groupHeader:hover { + @apply bg-stone-50; +} + +.groupExpanded { + @apply mb-2; +} + +.groupHeaderContent { + @apply flex justify-between items-center p-4; +} + +.groupHeaderLeft { + @apply flex items-center cursor-pointer; +} + +.groupHeaderRight { + @apply flex items-center gap-2; +} + +.groupExpandIcon { + @apply mr-3 transition-transform duration-200 text-storm-200; +} + +.groupExpanded .groupExpandIcon { + @apply rotate-90; +} + +.groupName { + @apply text-lg font-semibold text-storm-500 mr-3; +} + +.testCount { + @apply text-sm text-storm-200 font-medium; +} + +.inlineEditInput { + @apply bg-transparent border border-transparent text-base font-semibold text-storm-500 px-1 py-0.5 m-0 outline-none rounded transition-all; + min-width: 100px; +} + +.inlineEditInput:hover { + @apply bg-stone-50 border-stone-300; +} + +.inlineEditInput:focus { + @apply bg-stone-50 border-sky-400 shadow-sm; +} + +.groupActionBtn { + @apply p-2 text-sm rounded opacity-75 transition-opacity duration-200; +} + +.groupActionBtn:hover { + @apply opacity-100; +} + +.groupActionBtn.deleteBtn { + @apply opacity-0 transition-opacity duration-200; +} + +.groupHeader:hover .groupActionBtn.deleteBtn { + @apply opacity-75; +} + +.groupActionBtn.deleteBtn:hover { + @apply opacity-100; +} + +.runBtn { + @apply text-green-600; +} + +.runBtn:hover { + @apply text-green-400; +} + +.groupResult { + @apply flex items-center gap-1 px-2 py-1 rounded text-sm; +} + +.groupResultPassed { + @apply bg-green-100; +} + +.groupResultFailed { + @apply bg-red-100; +} + +.groupResultIcon { + @apply w-3 h-3; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx similarity index 76% rename from web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx rename to web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index ccbc7a4e2..822d39eca 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -7,7 +7,7 @@ import { Button } from "@dolthub/react-components"; import { useState, KeyboardEvent, ChangeEvent, MouseEvent } from "react"; import cx from "classnames"; import css from "./index.module.css"; -import ConfirmationModal from "./ConfirmationModal"; +import ConfirmationModal from "../ConfirmationModal"; type Props = { group: string; @@ -15,13 +15,23 @@ type Props = { onToggle: () => void; testCount: number; className?: string; - groupResult?: 'passed' | 'failed'; + groupResult?: "passed" | "failed"; onRunGroup: () => void; onDeleteGroup: () => void; onRenameGroup?: (oldName: string, newName: string) => void; }; -export default function TestGroup({ group, isExpanded, onToggle, testCount, className, groupResult, onRunGroup, onDeleteGroup, onRenameGroup }: Props) { +export default function TestGroup({ + group, + isExpanded, + onToggle, + testCount, + className, + groupResult, + onRunGroup, + onDeleteGroup, + onRenameGroup, +}: Props) { const groupName = group || "No Group"; const [localGroupName, setLocalGroupName] = useState(groupName); const [isEditing, setIsEditing] = useState(false); @@ -60,9 +70,9 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, clas }; const handleInputKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { handleInputBlur(); - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { setLocalGroupName(groupName); setIsEditing(false); } @@ -75,7 +85,14 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, clas }; return ( -
    +
    @@ -86,25 +103,28 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, clas onBlur={handleInputBlur} onKeyDown={handleInputKeyDown} onFocus={() => setIsEditing(true)} - onClick={(e) => e.stopPropagation()} + onClick={e => e.stopPropagation()} /> ({testCount} tests)
    {groupResult && ( -
    - {groupResult === 'passed' ? ( +
    + {groupResult === "passed" ? ( <> - + Passed ) : ( <> - + Failed )} @@ -112,7 +132,7 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, clas )} @@ -121,18 +141,18 @@ export default function TestGroup({ group, isExpanded, onToggle, testCount, clas
    - + void; onUpdateTest: (field: keyof Test, value: string) => void; @@ -38,7 +38,7 @@ export default function TestItem({ onNameEdit, onNameBlur, onRunTest, - onDeleteTest + onDeleteTest, }: Props) { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -57,34 +57,45 @@ export default function TestItem({ }; return ( -
  • +
  • onNameEdit(e.target.value)} + onChange={e => onNameEdit(e.target.value)} onFocus={() => onNameEdit(test.testName)} onBlur={onNameBlur} - onClick={(e) => e.stopPropagation()} + onClick={e => e.stopPropagation()} placeholder="Test name" />
    {testResult && ( -
    - {testResult.status === 'passed' ? ( +
    + {testResult.status === "passed" ? ( <> - + Passed ) : ( <> - + Failed )} @@ -113,8 +124,8 @@ export default function TestItem({
    {isExpanded && (
    - {testResult?.status === 'failed' && testResult.error && ( -
    + {testResult?.status === "failed" && testResult.error && ( +
    Error: {testResult.error}
    )} @@ -123,20 +134,24 @@ export default function TestItem({ label="Test Group" options={[ { value: "", label: "None" }, - ...groupOptions.filter(option => option !== "").map(option => {return { - value: option, - label: option - }}) + ...groupOptions + .filter(option => option !== "") + .map(option => { + return { + value: option, + label: option, + }; + }), ]} val={test.testGroup || ""} - onChangeValue={(value) => onUpdateTest('testGroup', value || "")} + onChangeValue={value => onUpdateTest("testGroup", value || "")} />
    onUpdateTest('testQuery', value)} + onChange={value => onUpdateTest("testQuery", value)} placeholder="Enter SQL query" />
    @@ -146,10 +161,15 @@ export default function TestItem({ options={[ { value: "expected_rows", label: "Expected Rows" }, { value: "expected_columns", label: "Expected Columns" }, - { value: "expected_single_value", label: "Expected Single Value" } + { + value: "expected_single_value", + label: "Expected Single Value", + }, ]} val={test.assertionType} - onChangeValue={(value) => onUpdateTest('assertionType', value || "")} + onChangeValue={value => + onUpdateTest("assertionType", value || "") + } />
    @@ -161,25 +181,27 @@ export default function TestItem({ { value: ">", label: ">" }, { value: "<", label: "<" }, { value: ">=", label: ">=" }, - { value: "<=", label: "<=" } + { value: "<=", label: "<=" }, ]} val={test.assertionComparator} - onChangeValue={(value) => onUpdateTest('assertionComparator', value || "")} + onChangeValue={value => + onUpdateTest("assertionComparator", value || "") + } />
    onUpdateTest('assertionValue', value)} + onChangeString={value => onUpdateTest("assertionValue", value)} placeholder="Expected result" className={css.fullWidthFormInput} - style={{ width: '100%' }} + style={{ width: "100%" }} />
    )} - + p { + @apply py-4; +} + +.loading { + @apply mx-12 my-4; +} + +.noTests { + @apply text-center text-lg m-6; +} + + +.fieldInput { + @apply w-full p-2 border border-stone-100 rounded text-sm; +} + +.fieldTextarea { + @apply w-full p-2 border border-stone-100 rounded text-sm min-h-[80px]; + resize: vertical; +} + +.fieldSelect { + @apply w-full p-2 border border-stone-100 rounded text-sm bg-white; +} + + + + +.groupedTests { + @apply mb-6; +} + +.groupedList { + @apply list-none p-0 m-0; +} + +.itemBottom { + @apply text-storm-200 font-normal flex flex-col-reverse; + a { + @apply font-normal; + } + + @screen lg { + @apply block; + } +} + +.bold { + @apply font-semibold; +} + +.delete { + @apply hidden; + + @screen lg { + @apply block ml-2; + } +} + +.button { + @apply mr-4; +} + + +.actionArea { + @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6; +} + +.createActions { + @apply flex gap-2; +} + +.createActions button { + @apply px-3 py-1.5 text-sm; +} + +.primaryActions { + @apply flex gap-3; +} + +.primaryActions button { + @apply px-4 py-2 text-sm font-medium; + height: 38px; + min-width: 120px; +} + +/* Create Dropdown Styles */ +.createButton { + @apply flex items-center justify-center px-4 py-2 text-sm font-medium bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; + height: 38px; + min-width: 120px; +} + +.createButton:focus { + @apply outline-none ring-2 ring-sky-500 ring-offset-2; +} + +.plus { + @apply mr-1.5; +} + +.caret { + @apply ml-1.5; +} + +.createPopup { + @apply bg-white border border-stone-300 rounded-md shadow-lg py-1 min-w-[140px]; +} + +.createPopupItem { + @apply m-0 p-0; +} + +.createPopupItem button { + @apply w-full text-left px-4 py-2 text-sm text-stone-700 hover:bg-stone-50 focus:bg-stone-50 transition-colors duration-150; + border: none; + background: transparent; + outline: none; +} + +.saveActions { + @apply flex gap-2 mt-4 pt-4 border-t border-stone-300; +} + +.saveActions button { + @apply px-2 py-1 text-xs; +} + + + + +.inlineGroupSelect { + @apply bg-transparent border border-stone-300 rounded text-sm px-2 py-1 ml-2 mr-2; + font-size: 13px; + width: auto; + min-width: 120px; +} + +.inlineGroupSelect:focus { + @apply border-sky-400 outline-none; +} + +/* Ungrouped Tests Styles */ +.ungroupedDivider { + @apply font-semibold text-center w-full my-8 text-base text-storm-500; +} + +.ungroupedDivider::before, +.ungroupedDivider::after { + content: ""; + @apply w-[46%] border-t border-stone-300 inline-block align-middle relative; +} + +.ungroupedDivider::after { + @apply right-0 -ml-[55%]; +} + +.ungroupedDivider::before { + @apply left-0 -mr-[55%]; +} + +.ungroupedTests { + @apply mb-6; +} + +/* Dynamic test status colors */ +.greenGroup { + @apply border-l-green-400 text-green-400; +} + +.redGroup { + @apply border-l-red-400 text-red-400; +} + +.orangeGroup { + @apply text-orange-500; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx similarity index 56% rename from web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx rename to web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx index 814f26db4..a572a6307 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx @@ -3,10 +3,10 @@ import { Button, Popup } from "@dolthub/react-components"; import { useState, useEffect, useCallback } from "react"; import cx from "classnames"; import css from "./index.module.css"; -import NewGroupModal from "./NewGroupModal"; -import TestGroup from "./TestGroup"; -import TestItem from "./TestItem"; -import ConfirmationModal from "./ConfirmationModal"; +import NewGroupModal from "../NewGroupModal"; +import TestGroup from "@pageComponents/DatabasePage/ForTests/TestGroup"; +import TestItem from "@pageComponents/DatabasePage/ForTests/TestItem"; +import ConfirmationModal from "../ConfirmationModal"; import { useTestList } from "./useTestList"; import { RefParams } from "@lib/params"; import { FaCaretDown } from "@react-icons/all-files/fa/FaCaretDown"; @@ -21,7 +21,7 @@ export default function TestList({ params }: Props) { const [showNewGroupModal, setShowNewGroupModal] = useState(false); const [newGroupName, setNewGroupName] = useState(""); const [hasHandledHash, setHasHandledHash] = useState(false); - + const { expandedItems, expandedGroups, @@ -61,10 +61,15 @@ export default function TestList({ params }: Props) { if (!targetTest) return; const containingGroup = Object.entries(groupedTests).find( - ([, groupTests]) => groupTests.some(test => test.testName === decodedHash), + ([, groupTests]) => + groupTests.some(test => test.testName === decodedHash), )?.[0]; - if (containingGroup && containingGroup !== "" && !expandedGroups.has(containingGroup)) { + if ( + containingGroup && + containingGroup !== "" && + !expandedGroups.has(containingGroup) + ) { toggleGroupExpanded(containingGroup); } @@ -76,15 +81,25 @@ export default function TestList({ params }: Props) { // scroll to the test after a short delay to ensure DOM is updated setTimeout(() => { - const testElement = document.querySelector(`[data-test-name="${decodedHash}"]`); + const testElement = document.querySelector( + `[data-test-name="${decodedHash}"]`, + ); if (testElement) { testElement.scrollIntoView({ - behavior: 'smooth', - block: 'center' + behavior: "smooth", + block: "center", }); } }, 100); - }, [tests, groupedTests, expandedGroups, expandedItems, toggleGroupExpanded, toggleExpanded, hasHandledHash]); + }, [ + tests, + groupedTests, + expandedGroups, + expandedItems, + toggleGroupExpanded, + toggleExpanded, + hasHandledHash, + ]); useEffect(() => { handleHashNavigation(); @@ -97,34 +112,40 @@ export default function TestList({ params }: Props) { } }; - const uniqueGroups = sortedGroupEntries.map(entry => entry[0]).filter(group => group !== "") + const uniqueGroups = sortedGroupEntries + .map(entry => entry[0]) + .filter(group => group !== ""); const getGroupStatusColors = (groupTests: any[]) => { - const groupTestResults = groupTests.map(test => testResults[test.testName]).filter(Boolean); - + const groupTestResults = groupTests + .map(test => testResults[test.testName]) + .filter(Boolean); + if (groupTestResults.length === 0) { return { red: false, green: false, orange: true }; } - - const hasFailures = groupTestResults.some(result => result && result.status === 'failed'); + + const hasFailures = groupTestResults.some( + result => result && result.status === "failed", + ); if (hasFailures) { return { red: true, green: false, orange: false }; } - + return { red: false, green: true, orange: false }; }; const getTestStatusColors = (testName: string) => { const testResult = testResults[testName]; - + if (!testResult) { return { red: false, green: false, orange: true }; } - - if (testResult.status === 'failed') { + + if (testResult.status === "failed") { return { red: true, green: false, orange: false }; } - + return { red: false, green: true, orange: false }; }; @@ -173,13 +194,13 @@ export default function TestList({ params }: Props) {
    - +
    @@ -195,7 +216,7 @@ export default function TestList({ params }: Props) {
  • - + - toggleGroupExpanded(groupName)} - testCount={groupTests.length} - className={cx({ - [css.greenGroup]: groupStatusColors.green, - [css.redGroup]: groupStatusColors.red, - [css.orangeGroup]: groupStatusColors.orange, - })} - groupResult={getGroupResult(groupName)} - onRunGroup={async () => await handleRunGroup(groupName)} - onDeleteGroup={async () => handleDeleteGroup(groupName)} - onRenameGroup={handleRenameGroup} - /> + toggleGroupExpanded(groupName)} + testCount={groupTests.length} + className={cx({ + [css.greenGroup]: groupStatusColors.green, + [css.redGroup]: groupStatusColors.red, + [css.orangeGroup]: groupStatusColors.orange, + })} + groupResult={getGroupResult(groupName)} + onRunGroup={async () => await handleRunGroup(groupName)} + onDeleteGroup={async () => handleDeleteGroup(groupName)} + onRenameGroup={handleRenameGroup} + /> {isGroupExpanded && (
      - {groupTests.map((test) => { - const testStatusColors = getTestStatusColors(test.testName); + {groupTests.map(test => { + const testStatusColors = getTestStatusColors( + test.testName, + ); return ( - toggleExpanded(test.testName)} - onUpdateTest={(field, value) => updateTest(test.testName, field, value)} - onNameEdit={(name) => handleTestNameEdit(test.testName, name)} - onNameBlur={() => handleTestNameBlur(test.testName)} - onRunTest={async () => await handleRunTest(test.testName)} - onDeleteTest={async () => await handleDeleteTest(test.testName)} - /> + + toggleExpanded(test.testName) + } + onUpdateTest={(field, value) => + updateTest(test.testName, field, value) + } + onNameEdit={name => + handleTestNameEdit(test.testName, name) + } + onNameBlur={() => + handleTestNameBlur(test.testName) + } + onRunTest={async () => + await handleRunTest(test.testName) + } + onDeleteTest={async () => + await handleDeleteTest(test.testName) + } + /> ); })}
    @@ -260,33 +295,43 @@ export default function TestList({ params }: Props) {
    ); })} -{(groupedTests[""] ?? []).length > 0 && ( + {(groupedTests[""] ?? []).length > 0 && ( <>
    Ungrouped
      - {groupedTests[""].map((test) => { - const testStatusColors = getTestStatusColors(test.testName); + {groupedTests[""].map(test => { + const testStatusColors = getTestStatusColors( + test.testName, + ); return ( - toggleExpanded(test.testName)} - onUpdateTest={(field, value) => updateTest(test.testName, field, value)} - onNameEdit={(name) => handleTestNameEdit(test.testName, name)} - onNameBlur={() => handleTestNameBlur(test.testName)} - onRunTest={async () => await handleRunTest(test.testName)} - onDeleteTest={async () => await handleDeleteTest(test.testName)} - /> + toggleExpanded(test.testName)} + onUpdateTest={(field, value) => + updateTest(test.testName, field, value) + } + onNameEdit={name => + handleTestNameEdit(test.testName, name) + } + onNameBlur={() => handleTestNameBlur(test.testName)} + onRunTest={async () => + await handleRunTest(test.testName) + } + onDeleteTest={async () => + await handleDeleteTest(test.testName) + } + /> ); })}
    @@ -296,11 +341,9 @@ export default function TestList({ params }: Props) {
    ) : ( -

    - No tests found -

    +

    No tests found

    )} - + >(new Set()); - const [expandedGroups, setExpandedGroups] = useState>( - new Set() - ); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [editingTestNames, setEditingTestNames] = useState< Record >({}); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [tests, setTests] = useState([]); const [emptyGroups, setEmptyGroups] = useState>(new Set()); - const [testResults, setTestResults] = useState>({}); + const [testResults, setTestResults] = useState< + Record + >({}); const [showUnsavedModal, setShowUnsavedModal] = useState(false); - const [pendingNavigation, setPendingNavigation] = useState(null); + const [pendingNavigation, setPendingNavigation] = useState( + null, + ); const autoRunExecutedRef = useRef(false); + const getResults = (testResults: TestResult[]): Record => { + const results: Record = {}; + for (const testResult of testResults) { + if (testResult.status === "PASS") { + results[testResult.testName] = { + status: "passed", + }; + } else { + results[testResult.testName] = { + status: "failed", + error: testResult.message, + }; + } + } + return results; + } + useEffect(() => { if (data?.tests.list) { - const initialTests = data.tests.list.map(({ __typename, ...test }) => test); + const initialTests = data.tests.list.map( + ({ __typename, ...test }) => test, + ); setTests(initialTests); - }}, [data?.tests.list]); + } + }, [data?.tests.list]); const handleConfirmNavigation = () => { if (pendingNavigation) { setShowUnsavedModal(false); - + const url = pendingNavigation; setPendingNavigation(null); setHasUnsavedChanges(false); // Clear unsaved changes to allow navigation - + setTimeout(async () => { await router.push(url); }); @@ -56,8 +83,7 @@ export function useTestList(params: RefParams) { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (hasUnsavedChanges) { e.preventDefault(); - e.returnValue = 'You have unsaved changes. Are you sure you want to leave?'; - return 'You have unsaved changes. Are you sure you want to leave?'; + return "You have unsaved changes. Are you sure you want to leave?"; } }; @@ -65,40 +91,31 @@ export function useTestList(params: RefParams) { if (hasUnsavedChanges && router.asPath !== url) { setPendingNavigation(url); setShowUnsavedModal(true); - router.events.emit('routeChangeError'); - throw 'Route change aborted by user'; + router.events.emit("routeChangeError"); + throw "Route change aborted by user"; } }; - window.addEventListener('beforeunload', handleBeforeUnload); - - router.events.on('routeChangeStart', handleRouteChangeStart); + window.addEventListener("beforeunload", handleBeforeUnload); + + router.events.on("routeChangeStart", handleRouteChangeStart); return () => { - window.removeEventListener('beforeunload', handleBeforeUnload); - router.events.off('routeChangeStart', handleRouteChangeStart); + window.removeEventListener("beforeunload", handleBeforeUnload); + router.events.off("routeChangeStart", handleRouteChangeStart); }; }, [hasUnsavedChanges, router]); useEffect(() => { - const shouldRunTests = router.query.runTests === 'true'; - console.log('DEBUG: Auto-run check:', { - shouldRunTests, - testsLength: tests.length, - autoRunExecuted: autoRunExecutedRef.current, - routerQuery: router.query, - pathname: router.pathname - }); - + const shouldRunTests = router.query.runTests === "true"; + if (shouldRunTests) { autoRunExecutedRef.current = false; } - + if (shouldRunTests && tests.length > 0 && !autoRunExecutedRef.current) { - console.log('DEBUG: Starting auto-run of tests'); const runAllTests = async () => { - autoRunExecutedRef.current = true; // Prevent re-running - console.log('DEBUG: Executing runTests GraphQL mutation'); + autoRunExecutedRef.current = true; try { const result = await runTests({ variables: { @@ -107,41 +124,32 @@ export function useTestList(params: RefParams) { }, }); - console.log('DEBUG: runTests result:', result); const testResultsList = result.data?.runTests.list || []; - const allResults: Record = {}; - - for (const testResult of testResultsList) { - if (testResult.status === 'PASS') { - allResults[testResult.testName] = { - status: 'passed' - } - } else { - allResults[testResult.testName] = { - status: 'failed', - error: testResult.message - } - } - } + const allResults = getResults(testResultsList); - console.log('DEBUG: Setting test results:', allResults); setTestResults(allResults); - } catch (error) { - console.error('Error auto-running tests:', error); + console.error("Error auto-running tests:", error); } }; void runAllTests(); } - - }, [router.query.runTests, tests.length, router, runTests, params.databaseName, params.refName, data?.tests.list]); + }, [ + router.query.runTests, + tests.length, + router, + runTests, + params.databaseName, + params.refName, + data?.tests.list, + ]); const [saveTestsMutation] = useSaveTestsMutation({ variables: { databaseName: params.databaseName, refName: params.refName, - tests: { list: tests } + tests: { list: tests }, }, }); @@ -189,21 +197,24 @@ export function useTestList(params: RefParams) { databaseName: params.databaseName, refName: params.refName, identifiers: { - values: [testName] - } + values: [testName], + }, }, - }) + }); - const testPassed = result.data && result.data.runTests.list.length > 0 && result.data.runTests.list[0].status === 'PASS'; + const testPassed = + result.data && + result.data.runTests.list.length > 0 && + result.data.runTests.list[0].status === "PASS"; setTestResults(prev => { return { ...prev, [testName]: { - status: testPassed ? 'passed' : 'failed', - error: testPassed ? undefined : result.data?.runTests.list[0].message - } - } + status: testPassed ? "passed" : "failed", + error: testPassed ? undefined : result.data?.runTests.list[0].message, + }, + }; }); }; @@ -212,106 +223,89 @@ export function useTestList(params: RefParams) { await handleSaveAll(); } - const result = await runTests({ variables: { databaseName: params.databaseName, refName: params.refName, identifiers: { - values: [groupName] - } + values: [groupName], + }, }, - }) - result.data?.runTests.list.map(test => test.status === 'PASS' ? { - status: 'passed' - } : - { - status: 'failed', - error: test.message - }) - - const testResults = result.data && result.data.runTests.list.length > 0 ? result.data.runTests.list : []; - const groupResults: Record = {}; + }); + result.data?.runTests.list.map(test => + test.status === "PASS" + ? { + status: "passed", + } + : { + status: "failed", + error: test.message, + }, + ); - for (const testResult of testResults) { - if (testResult.status === 'PASS') { - groupResults[testResult.testName] = { - status: 'passed' - } - } else { - groupResults[testResult.testName] = { - status: 'failed', - error: testResult.message - } - } - } + const testResults = + result.data && result.data.runTests.list.length > 0 + ? result.data.runTests.list + : []; + const groupResults = getResults(testResults); setTestResults(prev => { return { - ...prev, - ...groupResults - }}); + ...prev, + ...groupResults, + }; + }); }; - const handleRunAll = async () => { + const handleRunAll = async () => { if (hasUnsavedChanges) { await handleSaveAll(); } - const result = await runTests({ variables: { databaseName: params.databaseName, refName: params.refName, }, - }) - - const testResults = result.data && result.data.runTests.list.length > 0 ? result.data.runTests.list : []; - const allResults: Record = {}; + }); - for (const testResult of testResults) { - if (testResult.status === 'PASS') { - allResults[testResult.testName] = { - status: 'passed' - } - } else { - allResults[testResult.testName] = { - status: 'failed', - error: testResult.message - } - } - } + const testResults = + result.data && result.data.runTests.list.length > 0 + ? result.data.runTests.list + : []; + const allResults = getResults(testResults); setTestResults(prev => { return { - ...prev, - ...allResults - }}); + ...prev, + ...allResults, + }; + }); }; const handleDeleteTest = async (testName: string) => { - setTests(tests.filter(test => test.testName !== testName)); - setExpandedItems(prev => { - const newSet = new Set(prev); - newSet.delete(testName); - return newSet; - }); - await handleSaveAll(); + setTests(tests.filter(test => test.testName !== testName)); + setExpandedItems(prev => { + const newSet = new Set(prev); + newSet.delete(testName); + return newSet; + }); + await handleSaveAll(); }; const handleDeleteGroup = async (groupName: string) => { - setTests(tests.filter(test => test.testGroup !== groupName)); - setExpandedGroups(prev => { - const newSet = new Set(prev); - newSet.delete(groupName); - return newSet; - }); - setEmptyGroups(prev => { - const newSet = new Set(prev); - newSet.delete(groupName); - return newSet; - }); - await handleSaveAll(); - } + setTests(tests.filter(test => test.testGroup !== groupName)); + setExpandedGroups(prev => { + const newSet = new Set(prev); + newSet.delete(groupName); + return newSet; + }); + setEmptyGroups(prev => { + const newSet = new Set(prev); + newSet.delete(groupName); + return newSet; + }); + await handleSaveAll(); + }; const handleCreateGroup = ( groupName: string, @@ -335,7 +329,7 @@ export function useTestList(params: RefParams) { const existingNames = tests.map(t => t.testName); let uniqueTestName = baseTestName; let counter = 1; - + while (existingNames.includes(uniqueTestName)) { uniqueTestName = `${baseTestName} ${counter}`; counter += 1; @@ -368,9 +362,11 @@ export function useTestList(params: RefParams) { if (groupName && !expandedGroups.has(groupName)) { setExpandedGroups(prev => new Set([...prev, groupName])); } - + setTimeout(() => { - const testElement = document.querySelector(`[data-test-name="${newTest.testName}"]`); + const testElement = document.querySelector( + `[data-test-name="${newTest.testName}"]`, + ); testElement?.scrollIntoView({ behavior: "smooth", block: "center" }); }, 100); }; @@ -426,11 +422,11 @@ export function useTestList(params: RefParams) { groups[groupName] ??= []; groups[groupName].push(test); }); - + emptyGroups.forEach(groupName => { groups[groupName] ??= []; }); - + return groups; }, [tests, emptyGroups]); @@ -461,13 +457,13 @@ export function useTestList(params: RefParams) { const getGroupResult = (groupName: string) => { const groupTests = groupedTests[groupName]; if (groupTests.length === 0) return undefined; - + const results = groupTests.map(test => testResults[test.testName]); - + if (results.some(result => !result)) return undefined; - - const allPassed = results.every(result => result?.status === 'passed'); - return allPassed ? 'passed' : 'failed'; + + const allPassed = results.every(result => result?.status === "passed"); + return allPassed ? "passed" : "failed"; }; return { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css deleted file mode 100644 index 35c85372a..000000000 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.module.css +++ /dev/null @@ -1,410 +0,0 @@ -.container { - @apply mx-6; -} - -.top { - @apply flex justify-between pt-6; -} - -.tagContainer { - @apply mr-4 pb-4 text-primary border-stone-100 border-l-[1.5px]; - - @screen lg { - @apply ml-16; - } -} - -.list { - @apply mt-10 pl-6 pr-2; - - @screen lg { - @apply px-10; - } -} - -.item { - @apply border-2 border-stone-50 rounded-lg my-3 py-4 px-6 text-sm relative; -} - -.header { - @apply relative font-semibold pb-2; -} - -.bullet { - @apply border-stone-100 h-3 w-3 absolute bg-white rounded-full border-[1.5px] -left-[1.9rem] top-[0.4rem]; - - @screen lg { - @apply -left-[2.9rem]; - } -} - -.latest { - @apply inline-block px-4 mx-4 rounded-full bg-white border-2 border-green-400 text-green-400 text-sm font-semibold py-[0.05rem]; -} - -.itemTop { - @apply flex justify-between items-center cursor-pointer; -} - -.timeago { - @apply text-storm-200; -} - -.commitId { - @apply font-semibold text-storm-200 mb-1.5; - - &:hover { - @apply text-stone-100; - } - - @screen lg { - @apply mx-4 mb-0; - } -} - -.commitIcon { - @apply hidden; - - @screen lg { - @apply inline; - } -} - -.releaseName { - @apply pb-2 max-w-4xl text-base; -} - -.releaseNotes > p { - @apply py-4; -} - -.loading { - @apply mx-12 my-4; -} - -.noTests { - @apply text-center text-lg m-6; -} - -.expandedContent { - @apply mt-4 p-4 bg-stone-50 rounded; -} - -.fieldGroup { - @apply mb-4; -} - -.fieldLabel { - @apply block text-sm font-semibold mb-2 text-storm-200; -} - -.fieldInput { - @apply w-full p-2 border border-stone-100 rounded text-sm; -} - -.fieldTextarea { - @apply w-full p-2 border border-stone-100 rounded text-sm min-h-[80px]; - resize: vertical; -} - -.fieldSelect { - @apply w-full p-2 border border-stone-100 rounded text-sm bg-white; -} - -.fullWidthFormInput { - @apply w-full !important; - width: 100% !important; - min-width: 100% !important; - max-width: 100% !important; -} - -.testName { - @apply pb-2 text-base flex items-center; - flex: 1 1 auto; - min-width: 0; -} - -.expandIcon { - @apply mr-2 transition-transform duration-200 text-storm-200; -} - -.expanded .expandIcon { - @apply rotate-90; -} - -/* Test Group Styles */ -.groupHeader { - @apply border-l-4 bg-stone-50 border-stone-300 rounded-r mb-4 cursor-pointer; - transition: all 0.2s ease; -} - -.groupHeader:hover { - @apply bg-stone-50; -} - -.groupExpanded { - @apply mb-2; -} - -.groupHeaderContent { - @apply flex justify-between items-center p-4; -} - -.groupHeaderLeft { - @apply flex items-center cursor-pointer; -} - -.groupHeaderRight { - @apply flex items-center gap-2; -} - -.groupActionBtn { - @apply p-2 text-sm rounded opacity-75 transition-opacity duration-200; -} - -.groupActionBtn.deleteBtn { - @apply opacity-0 transition-opacity duration-200; -} - -.groupHeader:hover .groupActionBtn.deleteBtn { - @apply opacity-75; -} - -.groupActionBtn.deleteBtn:hover { - @apply opacity-100; -} - -.groupExpandIcon { - @apply mr-3 transition-transform duration-200 text-storm-200; -} - -.groupExpanded .groupExpandIcon { - @apply rotate-90; -} - -.groupName { - @apply text-lg font-semibold text-storm-500 mr-3; -} - -.testCount { - @apply text-sm text-storm-200 font-medium; -} - -.groupColorIndicator { - @apply w-3 h-3 rounded-full; -} - -.groupedItem { - @apply ml-8; -} - -.groupedTests { - @apply mb-6; -} - -.groupedList { - @apply list-none p-0 m-0; -} - -.itemBottom { - @apply text-storm-200 font-normal flex flex-col-reverse; - a { - @apply font-normal; - } - - @screen lg { - @apply block; - } -} - -.bold { - @apply font-semibold; -} - -.delete { - @apply hidden; - - @screen lg { - @apply block ml-2; - } -} - -.button { - @apply mr-4; -} - -/* Action Button Styles */ - -.testActionBtn { - @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200 ml-1; -} - -.testActionBtn:hover { - @apply opacity-100; -} - -.testActions { - @apply flex items-center gap-1 ml-auto opacity-0 transition-opacity duration-200; -} - -.item:hover .testActions { - @apply opacity-100; -} - -.actionArea { - @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6; -} - -.createActions { - @apply flex gap-2; -} - -.createActions button { - @apply px-3 py-1.5 text-sm; -} - -.primaryActions { - @apply flex gap-3; -} - -.primaryActions button { - @apply px-4 py-2 text-sm font-medium; - height: 38px; - min-width: 120px; -} - -/* Create Dropdown Styles */ -.createButton { - @apply flex items-center justify-center px-4 py-2 text-sm font-medium bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; - height: 38px; - min-width: 120px; -} - -.createButton:focus { - @apply outline-none ring-2 ring-sky-500 ring-offset-2; -} - -.plus { - @apply mr-1.5; -} - -.caret { - @apply ml-1.5; -} - -.createPopup { - @apply bg-white border border-stone-300 rounded-md shadow-lg py-1 min-w-[140px]; -} - -.createPopupItem { - @apply m-0 p-0; -} - -.createPopupItem button { - @apply w-full text-left px-4 py-2 text-sm text-stone-700 hover:bg-stone-50 focus:bg-stone-50 transition-colors duration-150; - border: none; - background: transparent; - outline: none; -} - -.saveActions { - @apply flex gap-2 mt-4 pt-4 border-t border-stone-300; -} - -.saveActions button { - @apply px-2 py-1 text-xs; -} - -.runBtn { - @apply text-green-600; -} - -.runBtn:hover { - @apply text-green-400; -} - -.inlineEditInput { - @apply bg-transparent border border-transparent text-base font-semibold text-storm-500 px-1 py-0.5 m-0 outline-none rounded transition-all; - min-width: 100px; -} - -.inlineEditInput:hover { - @apply bg-stone-50 border-stone-300; -} - -.inlineEditInput:focus { - @apply bg-stone-50 border-sky-400 shadow-sm; -} - -.editableTestName { - @apply bg-transparent border border-transparent text-base px-1 py-0.5 m-0 outline-none rounded transition-all flex-1; - min-width: 150px; - cursor: text; -} - -.editableTestName:hover { - @apply text-sky-600 bg-stone-50 border-stone-300; -} - -.editableTestName:focus { - @apply bg-white border-sky-400 shadow-sm; -} - -.inlineGroupSelect { - @apply bg-transparent border border-stone-300 rounded text-sm px-2 py-1 ml-2 mr-2; - font-size: 13px; - width: auto; - min-width: 120px; -} - -.inlineGroupSelect:focus { - @apply border-sky-400 outline-none; -} - -/* Ungrouped Tests Styles */ -.ungroupedDivider { - @apply font-semibold text-center w-full my-8 text-base text-storm-500; -} - -.ungroupedDivider::before, -.ungroupedDivider::after { - content: ""; - @apply w-[46%] border-t border-stone-300 inline-block align-middle relative; -} - -.ungroupedDivider::after { - @apply right-0 -ml-[55%]; -} - -.ungroupedDivider::before { - @apply left-0 -mr-[55%]; -} - -.ungroupedTests { - @apply mb-6; -} - -/* Dynamic test status colors */ -.greenGroup { - @apply border-l-green-400 text-green-400; -} - -.redGroup { - @apply border-l-red-400 text-red-400; -} - -.orangeGroup { - @apply text-orange-500; -} - -/*.greenTest {*/ -/* @apply bg-green-50;*/ -/*}*/ - -/*.redTest {*/ -/* @apply bg-red-50;*/ -/*}*/ - -/*.orangeTest {*/ -/* @apply bg-orange-50;*/ -/*}*/ diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts index f3acdb55f..742ee9147 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts @@ -16,7 +16,11 @@ export const LIST_TESTS = gql` `; export const SAVE_TESTS = gql` - mutation SaveTests($databaseName: String!, $refName: String!, $tests: TestListArgs!) { + mutation SaveTests( + $databaseName: String! + $refName: String! + $tests: TestListArgs! + ) { saveTests(databaseName: $databaseName, refName: $refName, tests: $tests) { list { testName @@ -28,11 +32,19 @@ export const SAVE_TESTS = gql` } } } -` +`; export const RUN_TESTS = gql` - query RunTests($databaseName: String!, $refName: String!, $identifiers: TestIdentifierArgs) { - runTests(databaseName: $databaseName, refName: $refName, identifiers: $identifiers) { + query RunTests( + $databaseName: String! + $refName: String! + $identifiers: TestIdentifierArgs + ) { + runTests( + databaseName: $databaseName + refName: $refName + identifiers: $identifiers + ) { list { testName testGroupName @@ -42,5 +54,4 @@ export const RUN_TESTS = gql` } } } -` - +`; diff --git a/web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx b/web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx index e6f8cb3e0..6b32bf92f 100644 --- a/web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx +++ b/web/renderer/pages/database/[databaseName]/tests/[refName]/index.tsx @@ -20,8 +20,8 @@ const DatabaseTestsPage: NextPage = ({ params }) => ( // #!if !isElectron export const getServerSideProps: GetServerSideProps = async ({ - params, - }) => { + params, +}) => { return { props: { params: params as RefParams }, }; From fc5c5e7ec5991c78e471cb65bf129ca83c1a8a64 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Wed, 17 Sep 2025 18:04:34 -0700 Subject: [PATCH 08/34] Address UI feedback --- .../TestResultsListItem/index.module.css | 4 + .../ConfirmationModal/index.module.css | 24 +- .../ForTests/ConfirmationModal/index.tsx | 24 +- .../ForTests/NewGroupModal/index.tsx | 53 ++-- .../ForTests/QueryEditor/index.module.css | 2 +- .../ForTests/TestGroup/index.module.css | 28 +- .../DatabasePage/ForTests/TestGroup/index.tsx | 15 + .../ForTests/TestItem/index.module.css | 34 ++- .../DatabasePage/ForTests/TestItem/index.tsx | 23 +- .../ForTests/TestList/CreateDropdown.tsx | 51 ++++ .../ForTests/TestList/TestItemRenderer.tsx | 63 ++++ .../ForTests/TestList/index.module.css | 47 +-- .../DatabasePage/ForTests/TestList/index.tsx | 271 +++--------------- .../ForTests/TestList/statusUtils.ts | 53 ++++ .../ForTests/TestList/useTestList.ts | 164 +++++------ 15 files changed, 441 insertions(+), 415 deletions(-) create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts diff --git a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css index 59b7d07ba..c61498931 100644 --- a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css +++ b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css @@ -2,6 +2,10 @@ @apply border-b border-stone-300 py-3 px-4 flex items-start gap-3 hover:bg-stone-50 transition-colors; } +li:last-child .itemContainer { + @apply border-b-0; +} + .icon { @apply flex-shrink-0 mt-0.5; svg { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css index a4147ebca..50a1c8d49 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css @@ -1,47 +1,47 @@ .overlay { - @apply fixed inset-0 bg-stone-700 bg-opacity-50 flex items-center justify-center z-50; + @apply fixed inset-0 bg-stone-700 bg-opacity-50 flex items-center justify-center z-50; } .modal { - @apply bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-hidden; + @apply bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-hidden; } .header { - @apply flex justify-between items-center p-4 border-b border-stone-300; + @apply flex justify-between items-center p-4 border-b border-stone-300; } .headerLeft { - @apply flex items-center gap-3; + @apply flex items-center gap-3; } .title { - @apply text-lg font-semibold text-stone-700 m-0; + @apply text-lg font-semibold text-stone-700 m-0; } .warningIcon { - @apply text-red-500 text-xl; + @apply text-red-500 text-xl; } .closeButton { - @apply p-1 text-stone-400 hover:text-stone-600 transition-colors bg-transparent border-none cursor-pointer rounded; + @apply p-1 text-stone-400 hover:text-stone-600 transition-colors bg-transparent border-none cursor-pointer rounded; } .body { - @apply p-4; + @apply p-4; } .message { - @apply text-stone-600 m-0 leading-relaxed; + @apply text-stone-600 m-0 leading-relaxed; } .footer { - @apply flex justify-end gap-3 p-4 border-t border-stone-300 bg-stone-50; + @apply flex justify-end gap-3 p-4 border-t border-stone-300 bg-stone-50; } .cancelButton { - @apply px-4 py-2 text-sm; + @apply px-4 py-2 text-sm; } .confirmButton { - @apply px-4 py-2 text-sm; + @apply px-4 py-2 text-sm; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx index 400dac4c4..86cb30ed6 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx @@ -15,25 +15,23 @@ type Props = { }; export default function ConfirmationModal({ - isOpen, - title, - message, - confirmText = "Confirm", - cancelText = "Cancel", - onConfirm, - onCancel, - destructive = false, -}: Props) { + isOpen, + title, + message, + confirmText = "Confirm", + cancelText = "Cancel", + onConfirm, + onCancel, + destructive = false, + }: Props) { if (!isOpen) return null; return (
    -
    e.stopPropagation()}> +
    e.stopPropagation()}>
    - {destructive && ( - - )} + {destructive && }

    {title}

    - -
    -
    -
    + + + ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css index ae332f6b4..1aaf86ca3 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css @@ -1,4 +1,4 @@ /* QueryEditor Component Styles */ .container { @apply bg-storm-600 rounded-sm border border-storm-500 overflow-hidden p-2; -} \ No newline at end of file +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css index 8b5c28773..d1c734834 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css @@ -1,5 +1,5 @@ .groupHeader { - @apply border-l-4 bg-stone-50 border-stone-300 rounded-r mb-4 cursor-pointer; + @apply bg-stone-50 rounded mb-2 cursor-pointer; transition: all 0.2s ease; } @@ -12,7 +12,7 @@ } .groupHeaderContent { - @apply flex justify-between items-center p-4; + @apply flex justify-between items-center py-3 pl-3 pr-9; } .groupHeaderLeft { @@ -20,7 +20,7 @@ } .groupHeaderRight { - @apply flex items-center gap-2; + @apply flex items-center gap-1; } .groupExpandIcon { @@ -32,7 +32,7 @@ } .groupName { - @apply text-lg font-semibold text-storm-500 mr-3; + @apply text-base font-semibold text-storm-500 mr-3; } .testCount { @@ -53,7 +53,7 @@ } .groupActionBtn { - @apply p-2 text-sm rounded opacity-75 transition-opacity duration-200; + @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200; } .groupActionBtn:hover { @@ -73,15 +73,29 @@ } .runBtn { - @apply text-green-600; + @apply text-green-600 mr-3; } .runBtn:hover { @apply text-green-400; } +.groupActionBtn.createBtn { + @apply text-sky-600 opacity-0 transition-opacity duration-200; +} + +.groupHeader:hover .groupActionBtn.createBtn { + @apply opacity-75; +} + +.groupActionBtn.createBtn:hover { + @apply text-sky-400 opacity-100; +} + .groupResult { - @apply flex items-center gap-1 px-2 py-1 rounded text-sm; + @apply flex items-center gap-1 px-2 py-1 rounded text-sm text-stone-700; + min-width: 80px; + justify-content: center; } .groupResultPassed { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index 822d39eca..baa69e93d 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -3,6 +3,7 @@ import { FaPlay } from "@react-icons/all-files/fa/FaPlay"; import { FaTrash } from "@react-icons/all-files/fa/FaTrash"; import { FaCheck } from "@react-icons/all-files/fa/FaCheck"; import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; +import { FaPlus } from "@react-icons/all-files/fa/FaPlus"; import { Button } from "@dolthub/react-components"; import { useState, KeyboardEvent, ChangeEvent, MouseEvent } from "react"; import cx from "classnames"; @@ -19,6 +20,7 @@ type Props = { onRunGroup: () => void; onDeleteGroup: () => void; onRenameGroup?: (oldName: string, newName: string) => void; + onCreateTest: (groupName: string) => void; }; export default function TestGroup({ @@ -31,6 +33,7 @@ export default function TestGroup({ onRunGroup, onDeleteGroup, onRenameGroup, + onCreateTest, }: Props) { const groupName = group || "No Group"; const [localGroupName, setLocalGroupName] = useState(groupName); @@ -47,6 +50,11 @@ export default function TestGroup({ setShowDeleteConfirm(true); }; + const handleCreateTest = (e: MouseEvent) => { + e.stopPropagation(); + onCreateTest(groupName); + }; + const handleConfirmDelete = () => { setShowDeleteConfirm(false); onDeleteGroup(); @@ -138,6 +146,13 @@ export default function TestGroup({ > + + + | null>(null); + + const debouncedOnUpdateTest = useCallback((field: keyof Test, value: string) => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + debounceRef.current = setTimeout(() => { + onUpdateTest(field, value); + }, 500); // 500ms debounce + }, [onUpdateTest]); + + useEffect(() => () => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + }, []); const handleDeleteClick = (e: MouseEvent) => { e.stopPropagation(); @@ -129,6 +145,7 @@ export default function TestItem({ Error: {testResult.error}
    )} +
    Query onUpdateTest("testQuery", value)} + onChange={value => debouncedOnUpdateTest("testQuery", value)} placeholder="Enter SQL query" />
    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx new file mode 100644 index 000000000..bf9fb26ae --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx @@ -0,0 +1,51 @@ +import { Popup } from "@dolthub/react-components"; +import { FaCaretDown } from "@react-icons/all-files/fa/FaCaretDown"; +import { FaCaretUp } from "@react-icons/all-files/fa/FaCaretUp"; +import { FaFile } from "@react-icons/all-files/fa/FaFile"; +import { FaFolder } from "@react-icons/all-files/fa/FaFolder"; +import { FiPlus } from "@react-icons/all-files/fi/FiPlus"; +import css from "./index.module.css"; + +type Props = { + onCreateTest: () => void; + onCreateGroup: () => void; +}; + +export default function CreateDropdown({ onCreateTest, onCreateGroup }: Props) { + return ( + ( + + )} + > +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    + ); +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx new file mode 100644 index 000000000..0a82caa21 --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx @@ -0,0 +1,63 @@ +import TestItem from "@pageComponents/DatabasePage/ForTests/TestItem"; +import { getTestStatusColors, getStatusClassName } from "./statusUtils"; +import css from "./index.module.css"; +import { Test } from "@gen/graphql-types"; + +type Props = { + tests: Test[]; + uniqueGroups: string[]; + expandedItems: Set; + editingTestNames: Record; + testResults: Record; + onToggleExpanded: (testName: string) => void; + onUpdateTest: (testName: string, field: keyof Test, value: string) => void; + onNameEdit: (testName: string, name: string) => void; + onNameBlur: (testName: string) => void; + onRunTest: (testName: string) => Promise; + onDeleteTest: (testName: string) => Promise; +}; + +export default function TestItemRenderer({ + tests, + uniqueGroups, + expandedItems, + editingTestNames, + testResults, + onToggleExpanded, + onUpdateTest, + onNameEdit, + onNameBlur, + onRunTest, + onDeleteTest, +}: Props) { + return ( + <> + {tests.map(test => { + const testStatusColors = getTestStatusColors(testResults[test.testName]); + const statusClassName = getStatusClassName(testStatusColors, { + green: css.greenTest, + red: css.redTest, + orange: css.orangeTest, + }); + + return ( + onToggleExpanded(test.testName)} + onUpdateTest={(field, value) => onUpdateTest(test.testName, field, value)} + onNameEdit={name => onNameEdit(test.testName, name)} + onNameBlur={() => onNameBlur(test.testName)} + onRunTest={async () => await onRunTest(test.testName)} + onDeleteTest={async () => await onDeleteTest(test.testName)} + /> + ); + })} + + ); +} \ No newline at end of file diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index 1adbc6b70..b7d2b48f5 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -1,5 +1,5 @@ .container { - @apply mx-6; + @apply mx-auto px-6; } .top { @@ -7,15 +7,11 @@ } .tagContainer { - @apply mr-4 pb-4 text-primary border-stone-100 border-l-[1.5px]; - - @screen lg { - @apply ml-16; - } + @apply pb-4 text-primary; } .list { - @apply mt-10 pl-6 pr-2; + @apply mt-10 px-6; @screen lg { @apply px-10; @@ -98,7 +94,7 @@ .groupedTests { - @apply mb-6; + @apply mb-4; } .groupedList { @@ -128,11 +124,6 @@ } } -.button { - @apply mr-4; -} - - .actionArea { @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6; } @@ -150,14 +141,14 @@ } .primaryActions button { - @apply px-4 py-2 text-sm font-medium; + @apply px-4 py-2 text-sm font-semibold; height: 38px; min-width: 120px; } /* Create Dropdown Styles */ .createButton { - @apply flex items-center justify-center px-4 py-2 text-sm font-medium bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; + @apply flex items-center justify-center px-4 py-2 text-sm font-semibold bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; height: 38px; min-width: 120px; } @@ -175,20 +166,24 @@ } .createPopup { - @apply bg-white border border-stone-300 rounded-md shadow-lg py-1 min-w-[140px]; + @apply text-primary; } .createPopupItem { - @apply m-0 p-0; + @apply mx-2 my-3 text-sm; } .createPopupItem button { - @apply w-full text-left px-4 py-2 text-sm text-stone-700 hover:bg-stone-50 focus:bg-stone-50 transition-colors duration-150; + @apply w-full text-left text-sky-600 hover:text-sky-700 font-semibold; border: none; background: transparent; outline: none; } +.createPopupItem button svg { + @apply inline-block mr-2.5 mb-0.5 text-lg; +} + .saveActions { @apply flex gap-2 mt-4 pt-4 border-t border-stone-300; } @@ -216,22 +211,8 @@ @apply font-semibold text-center w-full my-8 text-base text-storm-500; } -.ungroupedDivider::before, -.ungroupedDivider::after { - content: ""; - @apply w-[46%] border-t border-stone-300 inline-block align-middle relative; -} - -.ungroupedDivider::after { - @apply right-0 -ml-[55%]; -} - -.ungroupedDivider::before { - @apply left-0 -mr-[55%]; -} - .ungroupedTests { - @apply mb-6; + @apply mb-4; } /* Dynamic test status colors */ diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx index a572a6307..c3160f200 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx @@ -1,17 +1,14 @@ import HideForNoWritesWrapper from "@components/util/HideForNoWritesWrapper"; -import { Button, Popup } from "@dolthub/react-components"; -import { useState, useEffect, useCallback } from "react"; -import cx from "classnames"; +import { Button } from "@dolthub/react-components"; +import { useState, useEffect } from "react"; import css from "./index.module.css"; import NewGroupModal from "../NewGroupModal"; import TestGroup from "@pageComponents/DatabasePage/ForTests/TestGroup"; -import TestItem from "@pageComponents/DatabasePage/ForTests/TestItem"; -import ConfirmationModal from "../ConfirmationModal"; import { useTestList } from "./useTestList"; import { RefParams } from "@lib/params"; -import { FaCaretDown } from "@react-icons/all-files/fa/FaCaretDown"; -import { FaCaretUp } from "@react-icons/all-files/fa/FaCaretUp"; -import { FiPlus } from "@react-icons/all-files/fi/FiPlus"; +import CreateDropdown from "./CreateDropdown"; +import TestItemRenderer from "./TestItemRenderer"; +import { getGroupStatusColors, getStatusClassName } from "./statusUtils"; type Props = { params: RefParams; @@ -20,18 +17,15 @@ type Props = { export default function TestList({ params }: Props) { const [showNewGroupModal, setShowNewGroupModal] = useState(false); const [newGroupName, setNewGroupName] = useState(""); - const [hasHandledHash, setHasHandledHash] = useState(false); const { expandedItems, expandedGroups, editingTestNames, - hasUnsavedChanges, tests, groupedTests, sortedGroupEntries, testResults, - showUnsavedModal, getGroupResult, toggleExpanded, toggleGroupExpanded, @@ -43,63 +37,12 @@ export default function TestList({ params }: Props) { handleDeleteGroup, handleCreateGroup, handleCreateTest, - handleSaveAll, handleRenameGroup, handleTestNameEdit, handleTestNameBlur, - handleConfirmNavigation, - handleCancelNavigation, + handleHashNavigation, } = useTestList(params); - // Handle URL hash navigation to specific tests - const handleHashNavigation = useCallback(() => { - const hash = window.location.hash.slice(1); - if (!hash || tests.length === 0 || hasHandledHash) return; - - const decodedHash = decodeURIComponent(hash); - const targetTest = tests.find(test => test.testName === decodedHash); - if (!targetTest) return; - - const containingGroup = Object.entries(groupedTests).find( - ([, groupTests]) => - groupTests.some(test => test.testName === decodedHash), - )?.[0]; - - if ( - containingGroup && - containingGroup !== "" && - !expandedGroups.has(containingGroup) - ) { - toggleGroupExpanded(containingGroup); - } - - if (!expandedItems.has(decodedHash)) { - toggleExpanded(decodedHash); - } - - setHasHandledHash(true); - - // scroll to the test after a short delay to ensure DOM is updated - setTimeout(() => { - const testElement = document.querySelector( - `[data-test-name="${decodedHash}"]`, - ); - if (testElement) { - testElement.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - }, 100); - }, [ - tests, - groupedTests, - expandedGroups, - expandedItems, - toggleGroupExpanded, - toggleExpanded, - hasHandledHash, - ]); useEffect(() => { handleHashNavigation(); @@ -116,73 +59,7 @@ export default function TestList({ params }: Props) { .map(entry => entry[0]) .filter(group => group !== ""); - const getGroupStatusColors = (groupTests: any[]) => { - const groupTestResults = groupTests - .map(test => testResults[test.testName]) - .filter(Boolean); - - if (groupTestResults.length === 0) { - return { red: false, green: false, orange: true }; - } - - const hasFailures = groupTestResults.some( - result => result && result.status === "failed", - ); - if (hasFailures) { - return { red: true, green: false, orange: false }; - } - - return { red: false, green: true, orange: false }; - }; - - const getTestStatusColors = (testName: string) => { - const testResult = testResults[testName]; - - if (!testResult) { - return { red: false, green: false, orange: true }; - } - - if (testResult.status === "failed") { - return { red: true, green: false, orange: false }; - } - - return { red: false, green: true, orange: false }; - }; - const CreateDropdown = () => ( - ( - - )} - > -
    -
      -
    • - -
    • -
    • - -
    • -
    -
    -
    - ); return (
    @@ -191,26 +68,20 @@ export default function TestList({ params }: Props) {
    - + handleCreateTest()} + onCreateGroup={() => setShowNewGroupModal(true)} + />
    - - - {tests.length > 0 && ( )}
    @@ -231,7 +102,7 @@ export default function TestList({ params }: Props) { .filter(([groupName]) => groupName !== "") .map(([groupName, groupTests]) => { const isGroupExpanded = expandedGroups.has(groupName); - const groupStatusColors = getGroupStatusColors(groupTests); + const groupStatusColors = getGroupStatusColors(groupTests, testResults); return (
    @@ -240,56 +111,32 @@ export default function TestList({ params }: Props) { isExpanded={isGroupExpanded} onToggle={() => toggleGroupExpanded(groupName)} testCount={groupTests.length} - className={cx({ - [css.greenGroup]: groupStatusColors.green, - [css.redGroup]: groupStatusColors.red, - [css.orangeGroup]: groupStatusColors.orange, + className={getStatusClassName(groupStatusColors, { + green: css.greenGroup, + red: css.redGroup, + orange: css.orangeGroup, })} groupResult={getGroupResult(groupName)} onRunGroup={async () => await handleRunGroup(groupName)} onDeleteGroup={async () => handleDeleteGroup(groupName)} onRenameGroup={handleRenameGroup} + onCreateTest={(group) => handleCreateTest(group)} /> {isGroupExpanded && (
      - {groupTests.map(test => { - const testStatusColors = getTestStatusColors( - test.testName, - ); - return ( - - toggleExpanded(test.testName) - } - onUpdateTest={(field, value) => - updateTest(test.testName, field, value) - } - onNameEdit={name => - handleTestNameEdit(test.testName, name) - } - onNameBlur={() => - handleTestNameBlur(test.testName) - } - onRunTest={async () => - await handleRunTest(test.testName) - } - onDeleteTest={async () => - await handleDeleteTest(test.testName) - } - /> - ); - })} +
    )}
    @@ -300,40 +147,19 @@ export default function TestList({ params }: Props) {
    Ungrouped
      - {groupedTests[""].map(test => { - const testStatusColors = getTestStatusColors( - test.testName, - ); - return ( - toggleExpanded(test.testName)} - onUpdateTest={(field, value) => - updateTest(test.testName, field, value) - } - onNameEdit={name => - handleTestNameEdit(test.testName, name) - } - onNameBlur={() => handleTestNameBlur(test.testName)} - onRunTest={async () => - await handleRunTest(test.testName) - } - onDeleteTest={async () => - await handleDeleteTest(test.testName) - } - /> - ); - })} +
    @@ -343,17 +169,6 @@ export default function TestList({ params }: Props) { ) : (

    No tests found

    )} - -
    ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts new file mode 100644 index 000000000..b59796a3d --- /dev/null +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts @@ -0,0 +1,53 @@ +export type StatusColors = { + red: boolean; + green: boolean; + orange: boolean; +}; + +export function getTestStatusColors(testResult: any): StatusColors { + if (!testResult) { + return { red: false, green: false, orange: true }; + } + + if (testResult.status === "failed") { + return { red: true, green: false, orange: false }; + } + + return { red: false, green: true, orange: false }; +} + +export function getGroupStatusColors( + groupTests: any[], + testResults: Record, +): StatusColors { + const groupTestResults = groupTests + .map(test => testResults[test.testName]) + .filter(Boolean); + + if (groupTestResults.length === 0) { + return { red: false, green: false, orange: true }; + } + + const hasFailures = groupTestResults.some( + result => result && result.status === "failed", + ); + if (hasFailures) { + return { red: true, green: false, orange: false }; + } + + return { red: false, green: true, orange: false }; +} + +export function getStatusClassName( + statusColors: StatusColors, + cssClasses: { + green: string; + red: string; + orange: string; + }, +): string { + if (statusColors.green) return cssClasses.green; + if (statusColors.red) return cssClasses.red; + if (statusColors.orange) return cssClasses.orange; + return ""; +} \ No newline at end of file diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts index 99340731e..8847c5be4 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useRef } from "react"; +import { useState, useMemo, useEffect, useRef, useCallback } from "react"; import { Test, TestResult, useRunTestsLazyQuery, @@ -28,11 +28,21 @@ export function useTestList(params: RefParams) { const [testResults, setTestResults] = useState< Record >({}); - const [showUnsavedModal, setShowUnsavedModal] = useState(false); - const [pendingNavigation, setPendingNavigation] = useState( - null, - ); const autoRunExecutedRef = useRef(false); + const [hasHandledHash, setHasHandledHash] = useState(false); + + const [saveTestsMutation] = useSaveTestsMutation({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + tests: { list: tests }, + }, + }); + + const handleSaveAll = useCallback(async () => { + console.log("Saving all changes:", tests); + await saveTestsMutation(); + }, [saveTestsMutation, tests]); const getResults = (testResults: TestResult[]): Record => { const results: Record = {}; @@ -51,60 +61,22 @@ export function useTestList(params: RefParams) { return results; } + // Initialize tests from query data useEffect(() => { if (data?.tests.list) { - const initialTests = data.tests.list.map( - ({ __typename, ...test }) => test, - ); + const initialTests = data.tests.list.map(({ __typename, ...test }) => test); setTests(initialTests); - } - }, [data?.tests.list]); - - const handleConfirmNavigation = () => { - if (pendingNavigation) { - setShowUnsavedModal(false); - - const url = pendingNavigation; - setPendingNavigation(null); - setHasUnsavedChanges(false); // Clear unsaved changes to allow navigation - - setTimeout(async () => { - await router.push(url); - }); - } - }; - - const handleCancelNavigation = () => { - setShowUnsavedModal(false); - setPendingNavigation(null); - }; + }}, [data?.tests.list]); useEffect(() => { - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - if (hasUnsavedChanges) { - e.preventDefault(); - return "You have unsaved changes. Are you sure you want to leave?"; - } - }; - - const handleRouteChangeStart = (url: string) => { - if (hasUnsavedChanges && router.asPath !== url) { - setPendingNavigation(url); - setShowUnsavedModal(true); - router.events.emit("routeChangeError"); - throw "Route change aborted by user"; - } - }; - - window.addEventListener("beforeunload", handleBeforeUnload); + if (!hasUnsavedChanges) return; + const save = async () => { + await handleSaveAll(); + setHasUnsavedChanges(false); + } + void save(); + }, [hasUnsavedChanges, handleSaveAll]); - router.events.on("routeChangeStart", handleRouteChangeStart); - - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - router.events.off("routeChangeStart", handleRouteChangeStart); - }; - }, [hasUnsavedChanges, router]); useEffect(() => { const shouldRunTests = router.query.runTests === "true"; @@ -145,13 +117,6 @@ export function useTestList(params: RefParams) { data?.tests.list, ]); - const [saveTestsMutation] = useSaveTestsMutation({ - variables: { - databaseName: params.databaseName, - refName: params.refName, - tests: { list: tests }, - }, - }); const toggleExpanded = (id: string) => { const newExpanded = new Set(expandedItems); @@ -182,16 +147,9 @@ export function useTestList(params: RefParams) { setHasUnsavedChanges(true); }; - const handleSaveAll = async () => { - console.log("Saving all changes:", tests); - await saveTestsMutation(); - setHasUnsavedChanges(false); - }; + const handleRunTest = async (testName: string) => { - if (hasUnsavedChanges) { - await handleSaveAll(); - } const result = await runTests({ variables: { databaseName: params.databaseName, @@ -219,10 +177,6 @@ export function useTestList(params: RefParams) { }; const handleRunGroup = async (groupName: string) => { - if (hasUnsavedChanges) { - await handleSaveAll(); - } - const result = await runTests({ variables: { databaseName: params.databaseName, @@ -257,10 +211,6 @@ export function useTestList(params: RefParams) { }); }; const handleRunAll = async () => { - if (hasUnsavedChanges) { - await handleSaveAll(); - } - const result = await runTests({ variables: { databaseName: params.databaseName, @@ -289,7 +239,7 @@ export function useTestList(params: RefParams) { newSet.delete(testName); return newSet; }); - await handleSaveAll(); + setHasUnsavedChanges(true); }; const handleDeleteGroup = async (groupName: string) => { @@ -304,7 +254,7 @@ export function useTestList(params: RefParams) { newSet.delete(groupName); return newSet; }); - await handleSaveAll(); + setHasUnsavedChanges(true); }; const handleCreateGroup = ( @@ -318,7 +268,6 @@ export function useTestList(params: RefParams) { ) { setEmptyGroups(prev => new Set([...prev, groupName.trim()])); setExpandedGroups(prev => new Set([...prev, groupName.trim()])); - setHasUnsavedChanges(true); return true; } return false; @@ -337,8 +286,8 @@ export function useTestList(params: RefParams) { const newTest: Test = { testName: uniqueTestName, - testQuery: "", - assertionType: "expected_single_value", + testQuery: "SELECT * FROM tablename", + assertionType: "expected_rows", assertionComparator: "==", assertionValue: "", testGroup: groupName ?? "", @@ -466,6 +415,54 @@ export function useTestList(params: RefParams) { return allPassed ? "passed" : "failed"; }; + const handleHashNavigation = useCallback(() => { + const hash = window.location.hash.slice(1); + if (!hash || tests.length === 0 || hasHandledHash) return; + + const decodedHash = decodeURIComponent(hash); + const targetTest = tests.find(test => test.testName === decodedHash); + if (!targetTest) return; + + const containingGroup = Object.entries(groupedTests).find( + ([, groupTests]) => + groupTests.some(test => test.testName === decodedHash), + )?.[0]; + + if ( + containingGroup && + containingGroup !== "" && + !expandedGroups.has(containingGroup) + ) { + toggleGroupExpanded(containingGroup); + } + + if (!expandedItems.has(decodedHash)) { + toggleExpanded(decodedHash); + } + + setHasHandledHash(true); + + setTimeout(() => { + const testElement = document.querySelector( + `[data-test-name="${decodedHash}"]`, + ); + if (testElement) { + testElement.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + }, 100); + }, [ + tests, + groupedTests, + expandedGroups, + expandedItems, + toggleGroupExpanded, + toggleExpanded, + hasHandledHash, + ]); + return { expandedItems, expandedGroups, @@ -475,8 +472,6 @@ export function useTestList(params: RefParams) { groupedTests, sortedGroupEntries, testResults, - showUnsavedModal, - pendingNavigation, getGroupResult, toggleExpanded, toggleGroupExpanded, @@ -492,7 +487,6 @@ export function useTestList(params: RefParams) { handleRenameGroup, handleTestNameEdit, handleTestNameBlur, - handleConfirmNavigation, - handleCancelNavigation, + handleHashNavigation, }; } From 58745c4e4f58806da69c2041bc4e5aa61ce8f98b Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 10:51:46 -0700 Subject: [PATCH 09/34] more UI feedback --- .../TestResultsListItem/index.module.css | 4 ++-- .../ForTests/TestList/index.module.css | 15 +++++++-------- .../DatabasePage/ForTests/TestList/index.tsx | 9 +++++++++ web/renderer/lib/urls.ts | 6 ++++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css index c61498931..3fd325454 100644 --- a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css +++ b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css @@ -18,7 +18,7 @@ li:last-child .itemContainer { } .failureIcon { - @apply text-red-500; + @apply text-red-400; } .pendingIcon { @@ -42,7 +42,7 @@ li:last-child .itemContainer { } .red { - @apply bg-red-50 border-red-300; + @apply bg-coral-50 border-red-300; } .green { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index b7d2b48f5..b900adcfb 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -125,7 +125,7 @@ } .actionArea { - @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6; + @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-8 mb-6; } .createActions { @@ -133,7 +133,8 @@ } .createActions button { - @apply px-3 py-1.5 text-sm; + @apply px-3 py-1.5 text-sm font-semibold; + min-width: 100px; } .primaryActions { @@ -141,16 +142,14 @@ } .primaryActions button { - @apply px-4 py-2 text-sm font-semibold; - height: 38px; - min-width: 120px; + @apply px-3 py-1.5 text-sm; + min-width: 100px; } /* Create Dropdown Styles */ .createButton { - @apply flex items-center justify-center px-4 py-2 text-sm font-semibold bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; - height: 38px; - min-width: 120px; + @apply flex items-center justify-center px-3 py-1.5 text-sm font-semibold bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; + min-width: 100px; } .createButton:focus { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx index c3160f200..6692f2d15 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx @@ -9,6 +9,8 @@ import { RefParams } from "@lib/params"; import CreateDropdown from "./CreateDropdown"; import TestItemRenderer from "./TestItemRenderer"; import { getGroupStatusColors, getStatusClassName } from "./statusUtils"; +import Link from "@components/links/Link"; +import { workingDiff } from "@lib/urls"; type Props = { params: RefParams; @@ -73,6 +75,13 @@ export default function TestList({ params }: Props) { onCreateGroup={() => setShowNewGroupModal(true)} /> + + +
    diff --git a/web/renderer/lib/urls.ts b/web/renderer/lib/urls.ts index 4f06205c7..7128c2210 100644 --- a/web/renderer/lib/urls.ts +++ b/web/renderer/lib/urls.ts @@ -160,3 +160,9 @@ export const uploadStage = ( .addDynamic("stage", p.stage) .withQuery(q); }; + +export const workingDiff = (p: ps.RefParams): Route => + database(p) + .addStatic("compare") + .addDynamic("refName", p.refName, ENCODE) + .addStatic("STAGED..WORKING"); From b8d9711dcb77d48a45bd7ae227021e1a3255c23c Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 11:17:14 -0700 Subject: [PATCH 10/34] run prettier --- .../TestResultsForMergeList/index.tsx | 6 +- .../ForPulls/PullActions/Merge/index.tsx | 4 +- .../ConfirmationModal/index.module.css | 24 +++---- .../ForTests/ConfirmationModal/index.tsx | 24 ++++--- .../ForTests/NewGroupModal/index.module.css | 12 ++-- .../ForTests/NewGroupModal/index.tsx | 13 ++-- .../ForTests/QueryEditor/index.module.css | 2 +- .../ForTests/TestGroup/index.module.css | 62 ++++++++-------- .../DatabasePage/ForTests/TestGroup/index.tsx | 2 +- .../ForTests/TestItem/index.module.css | 70 +++++++++---------- .../DatabasePage/ForTests/TestItem/index.tsx | 28 +++++--- .../ForTests/TestList/TestItemRenderer.tsx | 15 ++-- .../ForTests/TestList/index.module.css | 9 --- .../DatabasePage/ForTests/TestList/index.tsx | 18 ++--- .../ForTests/TestList/statusUtils.ts | 2 +- .../ForTests/TestList/useTestList.ts | 27 ++++--- 16 files changed, 165 insertions(+), 153 deletions(-) diff --git a/web/renderer/components/TestResultsForMergeList/index.tsx b/web/renderer/components/TestResultsForMergeList/index.tsx index 40435386c..3cd5e66c5 100644 --- a/web/renderer/components/TestResultsForMergeList/index.tsx +++ b/web/renderer/components/TestResultsForMergeList/index.tsx @@ -1,4 +1,8 @@ -import { TestResult, useRunTestsLazyQuery, useTestListQuery } from "@gen/graphql-types"; +import { + TestResult, + useRunTestsLazyQuery, + useTestListQuery, +} from "@gen/graphql-types"; import { RefParams } from "@lib/params"; import { Button } from "@dolthub/react-components"; import cx from "classnames"; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx index 671334f19..31cc1cc6e 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx @@ -1,8 +1,6 @@ import HeaderUserCheckbox from "@components/HeaderUserCheckbox"; import { Button, SmallLoader } from "@dolthub/react-components"; -import { - PullDetailsFragment, -} from "@gen/graphql-types"; +import { PullDetailsFragment } from "@gen/graphql-types"; import useDatabaseDetails from "@hooks/useDatabaseDetails"; import { ApolloErrorType } from "@lib/errors/types"; import { PullDiffParams } from "@lib/params"; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css index 50a1c8d49..a4147ebca 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css @@ -1,47 +1,47 @@ .overlay { - @apply fixed inset-0 bg-stone-700 bg-opacity-50 flex items-center justify-center z-50; + @apply fixed inset-0 bg-stone-700 bg-opacity-50 flex items-center justify-center z-50; } .modal { - @apply bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-hidden; + @apply bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-hidden; } .header { - @apply flex justify-between items-center p-4 border-b border-stone-300; + @apply flex justify-between items-center p-4 border-b border-stone-300; } .headerLeft { - @apply flex items-center gap-3; + @apply flex items-center gap-3; } .title { - @apply text-lg font-semibold text-stone-700 m-0; + @apply text-lg font-semibold text-stone-700 m-0; } .warningIcon { - @apply text-red-500 text-xl; + @apply text-red-500 text-xl; } .closeButton { - @apply p-1 text-stone-400 hover:text-stone-600 transition-colors bg-transparent border-none cursor-pointer rounded; + @apply p-1 text-stone-400 hover:text-stone-600 transition-colors bg-transparent border-none cursor-pointer rounded; } .body { - @apply p-4; + @apply p-4; } .message { - @apply text-stone-600 m-0 leading-relaxed; + @apply text-stone-600 m-0 leading-relaxed; } .footer { - @apply flex justify-end gap-3 p-4 border-t border-stone-300 bg-stone-50; + @apply flex justify-end gap-3 p-4 border-t border-stone-300 bg-stone-50; } .cancelButton { - @apply px-4 py-2 text-sm; + @apply px-4 py-2 text-sm; } .confirmButton { - @apply px-4 py-2 text-sm; + @apply px-4 py-2 text-sm; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx index 86cb30ed6..400dac4c4 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx @@ -15,23 +15,25 @@ type Props = { }; export default function ConfirmationModal({ - isOpen, - title, - message, - confirmText = "Confirm", - cancelText = "Cancel", - onConfirm, - onCancel, - destructive = false, - }: Props) { + isOpen, + title, + message, + confirmText = "Confirm", + cancelText = "Cancel", + onConfirm, + onCancel, + destructive = false, +}: Props) { if (!isOpen) return null; return (
    -
    e.stopPropagation()}> +
    e.stopPropagation()}>
    - {destructive && } + {destructive && ( + + )}

    {title}

    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css index 1aaf86ca3..8f1b5d836 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css @@ -1,4 +1,4 @@ /* QueryEditor Component Styles */ .container { - @apply bg-storm-600 rounded-sm border border-storm-500 overflow-hidden p-2; + @apply bg-storm-600 rounded-sm border border-storm-500 overflow-hidden p-2; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css index d1c734834..6df66ede9 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css @@ -1,111 +1,111 @@ .groupHeader { - @apply bg-stone-50 rounded mb-2 cursor-pointer; - transition: all 0.2s ease; + @apply bg-stone-50 rounded mb-2 cursor-pointer; + transition: all 0.2s ease; } .groupHeader:hover { - @apply bg-stone-50; + @apply bg-stone-50; } .groupExpanded { - @apply mb-2; + @apply mb-2; } .groupHeaderContent { - @apply flex justify-between items-center py-3 pl-3 pr-9; + @apply flex justify-between items-center py-3 pl-3 pr-9; } .groupHeaderLeft { - @apply flex items-center cursor-pointer; + @apply flex items-center cursor-pointer; } .groupHeaderRight { - @apply flex items-center gap-1; + @apply flex items-center gap-1; } .groupExpandIcon { - @apply mr-3 transition-transform duration-200 text-storm-200; + @apply mr-3 transition-transform duration-200 text-storm-200; } .groupExpanded .groupExpandIcon { - @apply rotate-90; + @apply rotate-90; } .groupName { - @apply text-base font-semibold text-storm-500 mr-3; + @apply text-base font-semibold text-storm-500 mr-3; } .testCount { - @apply text-sm text-storm-200 font-medium; + @apply text-sm text-storm-200 font-medium; } .inlineEditInput { - @apply bg-transparent border border-transparent text-base font-semibold text-storm-500 px-1 py-0.5 m-0 outline-none rounded transition-all; - min-width: 100px; + @apply bg-transparent border border-transparent text-base font-semibold text-storm-500 px-1 py-0.5 m-0 outline-none rounded transition-all; + min-width: 100px; } .inlineEditInput:hover { - @apply bg-stone-50 border-stone-300; + @apply bg-stone-50 border-stone-300; } .inlineEditInput:focus { - @apply bg-stone-50 border-sky-400 shadow-sm; + @apply bg-stone-50 border-sky-400 shadow-sm; } .groupActionBtn { - @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200; + @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200; } .groupActionBtn:hover { - @apply opacity-100; + @apply opacity-100; } .groupActionBtn.deleteBtn { - @apply opacity-0 transition-opacity duration-200; + @apply opacity-0 transition-opacity duration-200; } .groupHeader:hover .groupActionBtn.deleteBtn { - @apply opacity-75; + @apply opacity-75; } .groupActionBtn.deleteBtn:hover { - @apply opacity-100; + @apply opacity-100; } .runBtn { - @apply text-green-600 mr-3; + @apply text-green-600 mr-3; } .runBtn:hover { - @apply text-green-400; + @apply text-green-400; } .groupActionBtn.createBtn { - @apply text-sky-600 opacity-0 transition-opacity duration-200; + @apply text-sky-600 opacity-0 transition-opacity duration-200; } .groupHeader:hover .groupActionBtn.createBtn { - @apply opacity-75; + @apply opacity-75; } .groupActionBtn.createBtn:hover { - @apply text-sky-400 opacity-100; + @apply text-sky-400 opacity-100; } .groupResult { - @apply flex items-center gap-1 px-2 py-1 rounded text-sm text-stone-700; - min-width: 80px; - justify-content: center; + @apply flex items-center gap-1 px-2 py-1 rounded text-sm text-stone-700; + min-width: 80px; + justify-content: center; } .groupResultPassed { - @apply bg-green-100; + @apply bg-green-100; } .groupResultFailed { - @apply bg-red-100; + @apply bg-red-100; } .groupResultIcon { - @apply w-3 h-3; + @apply w-3 h-3; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index baa69e93d..38b928596 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -122,7 +122,7 @@ export default function TestGroup({ css.groupResult, groupResult === "passed" ? css.groupResultPassed - : css.groupResultFailed + : css.groupResultFailed, )} > {groupResult === "passed" ? ( diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css index 360deb8cd..44c54f7fa 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css @@ -1,114 +1,114 @@ /* TestItem Component Styles */ .item { - @apply border-2 border-stone-50 rounded-lg my-2 py-3 px-4 text-sm relative; + @apply border-2 border-stone-50 rounded-lg my-2 py-3 px-4 text-sm relative; } .groupedItem { - @apply mx-4; + @apply mx-4; } .expanded .expandIcon { - @apply rotate-90; + @apply rotate-90; } .itemTop { - @apply flex items-center cursor-pointer gap-1; + @apply flex items-center cursor-pointer gap-1; } .testName { - @apply text-sm flex items-center; - flex: 1 1 auto; - min-width: 0; + @apply text-sm flex items-center; + flex: 1 1 auto; + min-width: 0; } .expandIcon { - @apply mr-3 transition-transform duration-200 text-storm-200; + @apply mr-3 transition-transform duration-200 text-storm-200; } .editableTestName { - @apply bg-transparent border border-transparent text-sm px-1 py-0.5 m-0 outline-none rounded transition-all flex-1; - min-width: 150px; - cursor: text; + @apply bg-transparent border border-transparent text-sm px-1 py-0.5 m-0 outline-none rounded transition-all flex-1; + min-width: 150px; + cursor: text; } .editableTestName:hover { - @apply text-sky-600 bg-stone-50 border-stone-300; + @apply text-sky-600 bg-stone-50 border-stone-300; } .editableTestName:focus { - @apply bg-white border-sky-400 shadow-sm; + @apply bg-white border-sky-400 shadow-sm; } .testActions { - @apply flex items-center gap-1 opacity-0 transition-opacity duration-200; + @apply flex items-center gap-1 opacity-0 transition-opacity duration-200; } .item:hover .testActions { - @apply opacity-100; + @apply opacity-100; } .testActionBtn { - @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200; + @apply p-1.5 text-xs rounded opacity-75 transition-opacity duration-200; } .testActionBtn:hover { - @apply opacity-100; + @apply opacity-100; } .runBtn { - @apply text-green-600 mr-10; + @apply text-green-600 mr-10; } .runBtn:hover { - @apply text-green-400; + @apply text-green-400; } .expandedContent { - @apply mt-4 px-4 pb-4 bg-white rounded; + @apply mt-4 px-4 pb-4 bg-white rounded; } .separator { - @apply border-t border-stone-300 mb-4 -mx-4; + @apply border-t border-stone-300 mb-4 -mx-4; } .fieldGroup { - @apply mb-4; + @apply mb-4; } .fieldLabel { - @apply block text-sm font-semibold mb-2 text-storm-500; + @apply block text-sm font-semibold mb-2 text-storm-500; } .fullWidthFormInput { - @apply w-full !important; - width: 100% !important; - min-width: 100% !important; - max-width: 100% !important; + @apply w-full !important; + width: 100% !important; + min-width: 100% !important; + max-width: 100% !important; } /* Test result status styles */ .testResult { - @apply flex items-center gap-1 px-2 py-1 rounded text-sm text-stone-700; - min-width: 80px; - justify-content: center; + @apply flex items-center gap-1 px-2 py-1 rounded text-sm text-stone-700; + min-width: 80px; + justify-content: center; } .testResultPassed { - @apply bg-green-100; + @apply bg-green-100; } .testResultFailed { - @apply bg-red-100; + @apply bg-red-100; } .testResultIcon { - @apply w-3 h-3; + @apply w-3 h-3; } .errorMessage { - @apply mb-4 p-3 bg-red-50 border border-red-400 rounded text-red-600 text-sm; + @apply mb-4 p-3 bg-red-50 border border-red-400 rounded text-red-600 text-sm; } .expandedContent :global(select) { - @apply relative z-10 bg-white + @apply relative z-10 bg-white; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx index 24e4c7b6d..5a01c81eb 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx @@ -43,20 +43,26 @@ export default function TestItem({ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const debounceRef = useRef | null>(null); - const debouncedOnUpdateTest = useCallback((field: keyof Test, value: string) => { - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - debounceRef.current = setTimeout(() => { - onUpdateTest(field, value); - }, 500); // 500ms debounce - }, [onUpdateTest]); + const debouncedOnUpdateTest = useCallback( + (field: keyof Test, value: string) => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + debounceRef.current = setTimeout(() => { + onUpdateTest(field, value); + }, 500); // 500ms debounce + }, + [onUpdateTest], + ); - useEffect(() => () => { + useEffect( + () => () => { if (debounceRef.current) { clearTimeout(debounceRef.current); } - }, []); + }, + [], + ); const handleDeleteClick = (e: MouseEvent) => { e.stopPropagation(); @@ -101,7 +107,7 @@ export default function TestItem({ css.testResult, testResult.status === "passed" ? css.testResultPassed - : css.testResultFailed + : css.testResultFailed, )} > {testResult.status === "passed" ? ( diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx index 0a82caa21..19a6f7234 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx @@ -8,7 +8,10 @@ type Props = { uniqueGroups: string[]; expandedItems: Set; editingTestNames: Record; - testResults: Record; + testResults: Record< + string, + { status: "passed" | "failed"; error?: string } | undefined + >; onToggleExpanded: (testName: string) => void; onUpdateTest: (testName: string, field: keyof Test, value: string) => void; onNameEdit: (testName: string, name: string) => void; @@ -33,7 +36,9 @@ export default function TestItemRenderer({ return ( <> {tests.map(test => { - const testStatusColors = getTestStatusColors(testResults[test.testName]); + const testStatusColors = getTestStatusColors( + testResults[test.testName], + ); const statusClassName = getStatusClassName(testStatusColors, { green: css.greenTest, red: css.redTest, @@ -50,7 +55,9 @@ export default function TestItemRenderer({ testResult={testResults[test.testName]} className={statusClassName} onToggleExpanded={() => onToggleExpanded(test.testName)} - onUpdateTest={(field, value) => onUpdateTest(test.testName, field, value)} + onUpdateTest={(field, value) => + onUpdateTest(test.testName, field, value) + } onNameEdit={name => onNameEdit(test.testName, name)} onNameBlur={() => onNameBlur(test.testName)} onRunTest={async () => await onRunTest(test.testName)} @@ -60,4 +67,4 @@ export default function TestItemRenderer({ })} ); -} \ No newline at end of file +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index b900adcfb..365e1052c 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -18,7 +18,6 @@ } } - .header { @apply relative font-semibold pb-2; } @@ -35,7 +34,6 @@ @apply inline-block px-4 mx-4 rounded-full bg-white border-2 border-green-400 text-green-400 text-sm font-semibold py-[0.05rem]; } - .timeago { @apply text-storm-200; } @@ -76,7 +74,6 @@ @apply text-center text-lg m-6; } - .fieldInput { @apply w-full p-2 border border-stone-100 rounded text-sm; } @@ -90,9 +87,6 @@ @apply w-full p-2 border border-stone-100 rounded text-sm bg-white; } - - - .groupedTests { @apply mb-4; } @@ -191,9 +185,6 @@ @apply px-2 py-1 text-xs; } - - - .inlineGroupSelect { @apply bg-transparent border border-stone-300 rounded text-sm px-2 py-1 ml-2 mr-2; font-size: 13px; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx index 6692f2d15..e5abb9ce9 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx @@ -45,7 +45,6 @@ export default function TestList({ params }: Props) { handleHashNavigation, } = useTestList(params); - useEffect(() => { handleHashNavigation(); }, [handleHashNavigation]); @@ -61,8 +60,6 @@ export default function TestList({ params }: Props) { .map(entry => entry[0]) .filter(group => group !== ""); - - return (
    @@ -75,12 +72,8 @@ export default function TestList({ params }: Props) { onCreateGroup={() => setShowNewGroupModal(true)} /> - - + +
    @@ -111,7 +104,10 @@ export default function TestList({ params }: Props) { .filter(([groupName]) => groupName !== "") .map(([groupName, groupTests]) => { const isGroupExpanded = expandedGroups.has(groupName); - const groupStatusColors = getGroupStatusColors(groupTests, testResults); + const groupStatusColors = getGroupStatusColors( + groupTests, + testResults, + ); return (
    @@ -129,7 +125,7 @@ export default function TestList({ params }: Props) { onRunGroup={async () => await handleRunGroup(groupName)} onDeleteGroup={async () => handleDeleteGroup(groupName)} onRenameGroup={handleRenameGroup} - onCreateTest={(group) => handleCreateTest(group)} + onCreateTest={group => handleCreateTest(group)} /> {isGroupExpanded && (
      diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts index b59796a3d..3f2059f4f 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts @@ -50,4 +50,4 @@ export function getStatusClassName( if (statusColors.red) return cssClasses.red; if (statusColors.orange) return cssClasses.orange; return ""; -} \ No newline at end of file +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts index 8847c5be4..c00a8a2bd 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts @@ -1,6 +1,7 @@ import { useState, useMemo, useEffect, useRef, useCallback } from "react"; import { - Test, TestResult, + Test, + TestResult, useRunTestsLazyQuery, useSaveTestsMutation, useTestListQuery, @@ -44,8 +45,13 @@ export function useTestList(params: RefParams) { await saveTestsMutation(); }, [saveTestsMutation, tests]); - const getResults = (testResults: TestResult[]): Record => { - const results: Record = {}; + const getResults = ( + testResults: TestResult[], + ): Record => { + const results: Record< + string, + { status: "passed" | "failed"; error?: string } + > = {}; for (const testResult of testResults) { if (testResult.status === "PASS") { results[testResult.testName] = { @@ -59,25 +65,27 @@ export function useTestList(params: RefParams) { } } return results; - } + }; // Initialize tests from query data useEffect(() => { if (data?.tests.list) { - const initialTests = data.tests.list.map(({ __typename, ...test }) => test); + const initialTests = data.tests.list.map( + ({ __typename, ...test }) => test, + ); setTests(initialTests); - }}, [data?.tests.list]); + } + }, [data?.tests.list]); useEffect(() => { if (!hasUnsavedChanges) return; const save = async () => { await handleSaveAll(); setHasUnsavedChanges(false); - } + }; void save(); }, [hasUnsavedChanges, handleSaveAll]); - useEffect(() => { const shouldRunTests = router.query.runTests === "true"; @@ -117,7 +125,6 @@ export function useTestList(params: RefParams) { data?.tests.list, ]); - const toggleExpanded = (id: string) => { const newExpanded = new Set(expandedItems); if (newExpanded.has(id)) { @@ -147,8 +154,6 @@ export function useTestList(params: RefParams) { setHasUnsavedChanges(true); }; - - const handleRunTest = async (testName: string) => { const result = await runTests({ variables: { From 793278123d9826ca2f8665d9fb25927d742bda6d Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 11:27:53 -0700 Subject: [PATCH 11/34] run prettier - graphql server --- .../queryFactory/dolt/doltEntityManager.ts | 27 +++++++++---------- graphql-server/src/queryFactory/dolt/index.ts | 5 ++-- .../src/queryFactory/dolt/queries.ts | 2 +- .../src/queryFactory/doltgres/index.ts | 6 ++--- .../src/queryFactory/doltgres/queries.ts | 7 +++-- graphql-server/src/queryFactory/index.ts | 1 - graphql-server/src/queryFactory/types.ts | 10 +++---- graphql-server/src/tests/test.model.ts | 2 +- graphql-server/src/tests/test.resolver.ts | 17 +++++++++--- graphql-server/src/utils/commonTypes.ts | 1 - 10 files changed, 41 insertions(+), 37 deletions(-) diff --git a/graphql-server/src/queryFactory/dolt/doltEntityManager.ts b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts index 08a0243e5..e6eae4218 100644 --- a/graphql-server/src/queryFactory/dolt/doltEntityManager.ts +++ b/graphql-server/src/queryFactory/dolt/doltEntityManager.ts @@ -186,9 +186,7 @@ export async function getDoltRemotesPaginated( .getRawMany(); } -export async function getDoltTests( - em: EntityManager, -): t.PR { +export async function getDoltTests(em: EntityManager): t.PR { return em .createQueryBuilder() .select("*") @@ -198,9 +196,9 @@ export async function getDoltTests( export async function saveDoltTests( em: EntityManager, - tests: TestArgs[] + tests: TestArgs[], ): Promise { - return em.transaction(async (transactionalEntityManager) => { + return em.transaction(async transactionalEntityManager => { await transactionalEntityManager .createQueryBuilder() .delete() @@ -213,16 +211,15 @@ export async function saveDoltTests( .into("dolt_tests") .values( tests.map(t => { - return { - test_name: t.testName, - test_group: t.testGroup, - test_query: t.testQuery, - assertion_type: t.assertionType, - assertion_comparator: t.assertionComparator, - assertion_value: t.assertionValue - } - } - ) + return { + test_name: t.testName, + test_group: t.testGroup, + test_query: t.testQuery, + assertion_type: t.assertionType, + assertion_comparator: t.assertionComparator, + assertion_value: t.assertionValue, + }; + }), ) .execute(); }); diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index d0550b37a..97cbef3bb 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -600,7 +600,7 @@ export class DoltQueryFactory args.identifiers?.values, args.databaseName, args.refName, - ) + ); } async saveTests(args: t.SaveTestsArgs): Promise { @@ -608,7 +608,7 @@ export class DoltQueryFactory async em => dem.saveDoltTests(em, args.tests.list), args.databaseName, args.refName, - ) + ); } } @@ -631,5 +631,4 @@ async function getTableInfoWithQR( foreignKeys: foreignKey.fromDoltRowsRes(fkRows), indexes: index.fromDoltRowsRes(idxRows), }; - } diff --git a/graphql-server/src/queryFactory/dolt/queries.ts b/graphql-server/src/queryFactory/dolt/queries.ts index 7191706d6..9c7b1bece 100644 --- a/graphql-server/src/queryFactory/dolt/queries.ts +++ b/graphql-server/src/queryFactory/dolt/queries.ts @@ -183,6 +183,6 @@ export const callCheckoutTable = `CALL DOLT_CHECKOUT(?)`; export const callDoltClone = `CALL DOLT_CLONE(?,?)`; export const doltTestRun = (argCount: number) => { - const placeholders = Array.from({ length: argCount }, () => `?`).join(', '); + const placeholders = Array.from({ length: argCount }, () => `?`).join(", "); return `SELECT * FROM DOLT_TEST_RUN(${placeholders})`; }; diff --git a/graphql-server/src/queryFactory/doltgres/index.ts b/graphql-server/src/queryFactory/doltgres/index.ts index 0ab7650eb..a47e8495b 100644 --- a/graphql-server/src/queryFactory/doltgres/index.ts +++ b/graphql-server/src/queryFactory/doltgres/index.ts @@ -574,7 +574,7 @@ export class DoltgresQueryFactory args.identifiers?.values, args.databaseName, args.refName, - ) + ); } async saveTests(args: t.SaveTestsArgs): Promise { @@ -582,11 +582,10 @@ export class DoltgresQueryFactory async em => dem.saveDoltTests(em, args.tests.list), args.databaseName, args.refName, - ) + ); } } - async function getTableInfoWithQR( qr: QueryRunner, args: t.TableMaybeSchemaArgs, @@ -609,4 +608,3 @@ async function getTableInfoWithQR( indexes: [], }; } - diff --git a/graphql-server/src/queryFactory/doltgres/queries.ts b/graphql-server/src/queryFactory/doltgres/queries.ts index 2c22b0414..c1f56bf9d 100644 --- a/graphql-server/src/queryFactory/doltgres/queries.ts +++ b/graphql-server/src/queryFactory/doltgres/queries.ts @@ -194,6 +194,9 @@ export const callFetchRemote = `SELECT DOLT_FETCH($1::text)`; export const callCreateBranchFromRemote = `CALL DOLT_BRANCH($1::text, $2::text)`; export const doltTestRun = (argCount: number) => { - const placeholders = Array.from({ length: argCount }, (_, i) => `$${i + 1}::text`).join(', '); + const placeholders = Array.from( + { length: argCount }, + (_, i) => `$${i + 1}::text`, + ).join(", "); return `SELECT * FROM DOLT_TEST_RUN(${placeholders})`; -} +}; diff --git a/graphql-server/src/queryFactory/index.ts b/graphql-server/src/queryFactory/index.ts index 0c1092a8f..64c8cab9d 100644 --- a/graphql-server/src/queryFactory/index.ts +++ b/graphql-server/src/queryFactory/index.ts @@ -186,5 +186,4 @@ export declare class QueryFactory { runTests(args: t.RunTestsArgs): t.PR; saveTests(args: t.SaveTestsArgs): Promise; - } diff --git a/graphql-server/src/queryFactory/types.ts b/graphql-server/src/queryFactory/types.ts index 136191f19..8c36b48d1 100644 --- a/graphql-server/src/queryFactory/types.ts +++ b/graphql-server/src/queryFactory/types.ts @@ -78,19 +78,19 @@ export type TestArgs = { assertionType: string; assertionComparator: string; assertionValue: string; -} +}; export type TestListArgs = { list: TestArgs[]; -} +}; export type SaveTestsArgs = RefArgs & { tests: TestListArgs; -} +}; export type TestIdentifierArgs = { values: string[]; -} +}; export type RunTestsArgs = RefArgs & { identifiers?: TestIdentifierArgs; -} +}; diff --git a/graphql-server/src/tests/test.model.ts b/graphql-server/src/tests/test.model.ts index 5b12031f1..e60446f32 100644 --- a/graphql-server/src/tests/test.model.ts +++ b/graphql-server/src/tests/test.model.ts @@ -71,5 +71,5 @@ export function fromDoltTestResultRowRes(testResult: RawRow): TestResult { query: testResult.query, status: testResult.status, message: testResult.message, - } + }; } diff --git a/graphql-server/src/tests/test.resolver.ts b/graphql-server/src/tests/test.resolver.ts index 340b9e0af..6f8771abf 100644 --- a/graphql-server/src/tests/test.resolver.ts +++ b/graphql-server/src/tests/test.resolver.ts @@ -1,12 +1,21 @@ import { - Args, ArgsType, Field, InputType, Mutation, + Args, + ArgsType, + Field, + InputType, + Mutation, Query, Resolver, } from "@nestjs/graphql"; import { ConnectionProvider } from "../connections/connection.provider"; -import { RefArgs } from "../utils/commonTypes"; -import { Test, TestList, fromDoltTestRowRes, TestResultList, fromDoltTestResultRowRes } from "./test.model"; - +import { RefArgs } from "../utils/commonTypes"; +import { + Test, + TestList, + fromDoltTestRowRes, + TestResultList, + fromDoltTestResultRowRes, +} from "./test.model"; @InputType() class TestArgs { diff --git a/graphql-server/src/utils/commonTypes.ts b/graphql-server/src/utils/commonTypes.ts index 17ecb30fa..950d0d160 100644 --- a/graphql-server/src/utils/commonTypes.ts +++ b/graphql-server/src/utils/commonTypes.ts @@ -98,4 +98,3 @@ export class AuthorInfo { @Field() email: string; } - From a76ffa2daf638997e13987b0d4656d2dcbc8328c Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 12:00:14 -0700 Subject: [PATCH 12/34] remove unused imports --- graphql-server/src/queryFactory/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/graphql-server/src/queryFactory/types.ts b/graphql-server/src/queryFactory/types.ts index 8c36b48d1..042007a95 100644 --- a/graphql-server/src/queryFactory/types.ts +++ b/graphql-server/src/queryFactory/types.ts @@ -1,7 +1,5 @@ import { SortBranchesBy } from "../branches/branch.enum"; import { DiffRowType } from "../rowDiffs/rowDiff.enums"; -import { Field } from "@nestjs/graphql"; -import { TestList } from "../tests/test.model"; export type DBArgs = { databaseName: string }; export type CloneArgs = DBArgs & { remoteDbPath: string }; From 3649ff669d9f0441fbc969bd762f13c43eb2b5a4 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 12:50:59 -0700 Subject: [PATCH 13/34] more styling improvements --- .../ConfirmationModal/index.module.css | 44 +-------------- .../ForTests/ConfirmationModal/index.tsx | 56 ++++++------------- .../ForTests/TestGroup/index.module.css | 4 ++ .../DatabasePage/ForTests/TestGroup/index.tsx | 21 +++---- .../DatabasePage/ForTests/TestItem/index.tsx | 3 +- .../ForTests/TestList/CreateDropdown.tsx | 18 +++++- .../ForTests/TestList/index.module.css | 1 - 7 files changed, 50 insertions(+), 97 deletions(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css index a4147ebca..58435f156 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.module.css @@ -1,47 +1,7 @@ -.overlay { - @apply fixed inset-0 bg-stone-700 bg-opacity-50 flex items-center justify-center z-50; -} - -.modal { - @apply bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-hidden; -} - -.header { - @apply flex justify-between items-center p-4 border-b border-stone-300; -} - -.headerLeft { - @apply flex items-center gap-3; -} - -.title { - @apply text-lg font-semibold text-stone-700 m-0; -} - -.warningIcon { - @apply text-red-500 text-xl; -} - -.closeButton { - @apply p-1 text-stone-400 hover:text-stone-600 transition-colors bg-transparent border-none cursor-pointer rounded; -} - -.body { - @apply p-4; +.modalInner { + @apply pb-2; } .message { @apply text-stone-600 m-0 leading-relaxed; } - -.footer { - @apply flex justify-end gap-3 p-4 border-t border-stone-300 bg-stone-50; -} - -.cancelButton { - @apply px-4 py-2 text-sm; -} - -.confirmButton { - @apply px-4 py-2 text-sm; -} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx index 400dac4c4..31fbf23b0 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/ConfirmationModal/index.tsx @@ -1,6 +1,9 @@ -import { Button } from "@dolthub/react-components"; -import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; -import { FaExclamationTriangle } from "@react-icons/all-files/fa/FaExclamationTriangle"; +import { + Button, + ModalButtons, + ModalInner, + ModalOuter, +} from "@dolthub/react-components"; import css from "./index.module.css"; type Props = { @@ -8,7 +11,6 @@ type Props = { title: string; message: string; confirmText?: string; - cancelText?: string; onConfirm: () => void; onCancel: () => void; destructive?: boolean; @@ -19,46 +21,20 @@ export default function ConfirmationModal({ title, message, confirmText = "Confirm", - cancelText = "Cancel", onConfirm, onCancel, destructive = false, }: Props) { - if (!isOpen) return null; - return ( -
      -
      e.stopPropagation()}> -
      -
      - {destructive && ( - - )} -

      {title}

      -
      - -
      - -
      -

      {message}

      -
      - -
      - - -
      -
      -
      + + +

      {message}

      +
      + + + +
      ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css index 6df66ede9..a9419219e 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css @@ -109,3 +109,7 @@ .groupResultIcon { @apply w-3 h-3; } + +.confirmDeleteModal { + @apply text-stone-700; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index 38b928596..df0284b54 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -164,16 +164,17 @@ export default function TestGroup({
    - +
    + +
    ); } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx index 5a01c81eb..b2a293a72 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx @@ -228,9 +228,8 @@ export default function TestItem({ (
  • - diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index 365e1052c..f8db9ffa2 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -205,7 +205,6 @@ @apply mb-4; } -/* Dynamic test status colors */ .greenGroup { @apply border-l-green-400 text-green-400; } From 63a9c525f28864fc6a55d02606a24c30a9cb2baf Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 13:41:24 -0700 Subject: [PATCH 14/34] get rid of inline styling --- web/renderer/components/TestResultsForMergeList/index.tsx | 4 +--- .../DatabasePage/ForTests/NewGroupModal/index.module.css | 1 - .../DatabasePage/ForTests/QueryEditor/index.module.css | 1 - .../DatabasePage/ForTests/TestItem/index.module.css | 1 - .../pageComponents/DatabasePage/ForTests/TestItem/index.tsx | 3 +-- .../DatabasePage/ForTests/TestList/index.module.css | 6 ++++-- .../pageComponents/DatabasePage/ForTests/TestList/index.tsx | 5 +---- 7 files changed, 7 insertions(+), 14 deletions(-) diff --git a/web/renderer/components/TestResultsForMergeList/index.tsx b/web/renderer/components/TestResultsForMergeList/index.tsx index 3cd5e66c5..08a65ce4b 100644 --- a/web/renderer/components/TestResultsForMergeList/index.tsx +++ b/web/renderer/components/TestResultsForMergeList/index.tsx @@ -136,9 +136,7 @@ export default function TestResultsForMergeList({ params }: Props) { )) ) : (
  • - - No test results available. Run tests to see results here. - + Run tests to see results here.
  • )} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css index 99e38ae7f..46121127e 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/NewGroupModal/index.module.css @@ -1,4 +1,3 @@ -/* NewGroupModal Component Styles */ .overlay { @apply fixed inset-0 bg-stone-700 bg-opacity-50 flex items-center justify-center z-50; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css index 8f1b5d836..7dac06636 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/QueryEditor/index.module.css @@ -1,4 +1,3 @@ -/* QueryEditor Component Styles */ .container { @apply bg-storm-600 rounded-sm border border-storm-500 overflow-hidden p-2; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css index 44c54f7fa..316e6c8c8 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css @@ -86,7 +86,6 @@ max-width: 100% !important; } -/* Test result status styles */ .testResult { @apply flex items-center gap-1 px-2 py-1 rounded text-sm text-stone-700; min-width: 80px; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx index b2a293a72..05bf0b850 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx @@ -129,7 +129,7 @@ export default function TestItem({ e.stopPropagation(); onRunTest(); }} - className={`${css.testActionBtn} ${css.runBtn}`} + className={cx(css.testActionBtn, css.runBtn)} data-tooltip-content="Run test" > @@ -219,7 +219,6 @@ export default function TestItem({ onChangeString={value => onUpdateTest("assertionValue", value)} placeholder="Expected result" className={css.fullWidthFormInput} - style={{ width: "100%" }} />
    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index f8db9ffa2..efc52bf72 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -140,7 +140,6 @@ min-width: 100px; } -/* Create Dropdown Styles */ .createButton { @apply flex items-center justify-center px-3 py-1.5 text-sm font-semibold bg-sky-600 text-white rounded hover:bg-sky-700 transition-colors duration-200; min-width: 100px; @@ -196,7 +195,6 @@ @apply border-sky-400 outline-none; } -/* Ungrouped Tests Styles */ .ungroupedDivider { @apply font-semibold text-center w-full my-8 text-base text-storm-500; } @@ -216,3 +214,7 @@ .orangeGroup { @apply text-orange-500; } + +.runAllButton { + @apply bg-green-600 text-white; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx index e5abb9ce9..b18930991 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx @@ -79,10 +79,7 @@ export default function TestList({ params }: Props) {
    {tests.length > 0 && ( - )} From 7d109884e968b171c83d35b82b0c92911bdf1a1b Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 13:59:36 -0700 Subject: [PATCH 15/34] factor out statusUtils class --- .../ForTests/TestList/TestItemRenderer.tsx | 15 +----- .../ForTests/TestList/index.module.css | 11 ---- .../DatabasePage/ForTests/TestList/index.tsx | 11 ---- .../ForTests/TestList/statusUtils.ts | 53 ------------------- .../ForTests/TestList/useTestList.ts | 22 ++++---- 5 files changed, 12 insertions(+), 100 deletions(-) delete mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx index 19a6f7234..f35014605 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx @@ -1,5 +1,4 @@ import TestItem from "@pageComponents/DatabasePage/ForTests/TestItem"; -import { getTestStatusColors, getStatusClassName } from "./statusUtils"; import css from "./index.module.css"; import { Test } from "@gen/graphql-types"; @@ -17,7 +16,7 @@ type Props = { onNameEdit: (testName: string, name: string) => void; onNameBlur: (testName: string) => void; onRunTest: (testName: string) => Promise; - onDeleteTest: (testName: string) => Promise; + onDeleteTest: (testName: string) => void; }; export default function TestItemRenderer({ @@ -36,15 +35,6 @@ export default function TestItemRenderer({ return ( <> {tests.map(test => { - const testStatusColors = getTestStatusColors( - testResults[test.testName], - ); - const statusClassName = getStatusClassName(testStatusColors, { - green: css.greenTest, - red: css.redTest, - orange: css.orangeTest, - }); - return ( onToggleExpanded(test.testName)} onUpdateTest={(field, value) => onUpdateTest(test.testName, field, value) @@ -61,7 +50,7 @@ export default function TestItemRenderer({ onNameEdit={name => onNameEdit(test.testName, name)} onNameBlur={() => onNameBlur(test.testName)} onRunTest={async () => await onRunTest(test.testName)} - onDeleteTest={async () => await onDeleteTest(test.testName)} + onDeleteTest={() => onDeleteTest(test.testName)} /> ); })} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index efc52bf72..f5cc39176 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -203,17 +203,6 @@ @apply mb-4; } -.greenGroup { - @apply border-l-green-400 text-green-400; -} - -.redGroup { - @apply border-l-red-400 text-red-400; -} - -.orangeGroup { - @apply text-orange-500; -} .runAllButton { @apply bg-green-600 text-white; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx index b18930991..b70cac562 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx @@ -8,7 +8,6 @@ import { useTestList } from "./useTestList"; import { RefParams } from "@lib/params"; import CreateDropdown from "./CreateDropdown"; import TestItemRenderer from "./TestItemRenderer"; -import { getGroupStatusColors, getStatusClassName } from "./statusUtils"; import Link from "@components/links/Link"; import { workingDiff } from "@lib/urls"; @@ -101,11 +100,6 @@ export default function TestList({ params }: Props) { .filter(([groupName]) => groupName !== "") .map(([groupName, groupTests]) => { const isGroupExpanded = expandedGroups.has(groupName); - const groupStatusColors = getGroupStatusColors( - groupTests, - testResults, - ); - return (
    toggleGroupExpanded(groupName)} testCount={groupTests.length} - className={getStatusClassName(groupStatusColors, { - green: css.greenGroup, - red: css.redGroup, - orange: css.orangeGroup, - })} groupResult={getGroupResult(groupName)} onRunGroup={async () => await handleRunGroup(groupName)} onDeleteGroup={async () => handleDeleteGroup(groupName)} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts deleted file mode 100644 index 3f2059f4f..000000000 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/statusUtils.ts +++ /dev/null @@ -1,53 +0,0 @@ -export type StatusColors = { - red: boolean; - green: boolean; - orange: boolean; -}; - -export function getTestStatusColors(testResult: any): StatusColors { - if (!testResult) { - return { red: false, green: false, orange: true }; - } - - if (testResult.status === "failed") { - return { red: true, green: false, orange: false }; - } - - return { red: false, green: true, orange: false }; -} - -export function getGroupStatusColors( - groupTests: any[], - testResults: Record, -): StatusColors { - const groupTestResults = groupTests - .map(test => testResults[test.testName]) - .filter(Boolean); - - if (groupTestResults.length === 0) { - return { red: false, green: false, orange: true }; - } - - const hasFailures = groupTestResults.some( - result => result && result.status === "failed", - ); - if (hasFailures) { - return { red: true, green: false, orange: false }; - } - - return { red: false, green: true, orange: false }; -} - -export function getStatusClassName( - statusColors: StatusColors, - cssClasses: { - green: string; - red: string; - orange: string; - }, -): string { - if (statusColors.green) return cssClasses.green; - if (statusColors.red) return cssClasses.red; - if (statusColors.orange) return cssClasses.orange; - return ""; -} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts index c00a8a2bd..89fe6bcfc 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts @@ -104,7 +104,7 @@ export function useTestList(params: RefParams) { }, }); - const testResultsList = result.data?.runTests.list || []; + const testResultsList = result.data?.runTests.list ?? []; const allResults = getResults(testResultsList); setTestResults(allResults); @@ -125,17 +125,17 @@ export function useTestList(params: RefParams) { data?.tests.list, ]); - const toggleExpanded = (id: string) => { + const toggleExpanded = useCallback((testName: string) => { const newExpanded = new Set(expandedItems); - if (newExpanded.has(id)) { - newExpanded.delete(id); + if (newExpanded.has(testName)) { + newExpanded.delete(testName); } else { - newExpanded.add(id); + newExpanded.add(testName); } setExpandedItems(newExpanded); - }; + }, [expandedItems]); - const toggleGroupExpanded = (groupName: string) => { + const toggleGroupExpanded = useCallback((groupName: string) => { const newExpandedGroups = new Set(expandedGroups); if (newExpandedGroups.has(groupName)) { newExpandedGroups.delete(groupName); @@ -143,7 +143,7 @@ export function useTestList(params: RefParams) { newExpandedGroups.add(groupName); } setExpandedGroups(newExpandedGroups); - }; + }, [expandedGroups]); const updateTest = (name: string, field: keyof Test, value: string) => { setTests( @@ -237,7 +237,7 @@ export function useTestList(params: RefParams) { }); }; - const handleDeleteTest = async (testName: string) => { + const handleDeleteTest = (testName: string) => { setTests(tests.filter(test => test.testName !== testName)); setExpandedItems(prev => { const newSet = new Set(prev); @@ -247,7 +247,7 @@ export function useTestList(params: RefParams) { setHasUnsavedChanges(true); }; - const handleDeleteGroup = async (groupName: string) => { + const handleDeleteGroup = (groupName: string) => { setTests(tests.filter(test => test.testGroup !== groupName)); setExpandedGroups(prev => { const newSet = new Set(prev); @@ -472,7 +472,6 @@ export function useTestList(params: RefParams) { expandedItems, expandedGroups, editingTestNames, - hasUnsavedChanges, tests, groupedTests, sortedGroupEntries, @@ -488,7 +487,6 @@ export function useTestList(params: RefParams) { handleDeleteGroup, handleCreateGroup, handleCreateTest, - handleSaveAll, handleRenameGroup, handleTestNameEdit, handleTestNameBlur, From e2629e175cb5b9a118271391e117a497b66fb9e7 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 14:00:23 -0700 Subject: [PATCH 16/34] prettier --- .../ForTests/TestList/index.module.css | 1 - .../ForTests/TestList/useTestList.ts | 44 +++++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index f5cc39176..53e70f2a7 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -203,7 +203,6 @@ @apply mb-4; } - .runAllButton { @apply bg-green-600 text-white; } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts index 89fe6bcfc..08e83ab93 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts @@ -125,25 +125,31 @@ export function useTestList(params: RefParams) { data?.tests.list, ]); - const toggleExpanded = useCallback((testName: string) => { - const newExpanded = new Set(expandedItems); - if (newExpanded.has(testName)) { - newExpanded.delete(testName); - } else { - newExpanded.add(testName); - } - setExpandedItems(newExpanded); - }, [expandedItems]); - - const toggleGroupExpanded = useCallback((groupName: string) => { - const newExpandedGroups = new Set(expandedGroups); - if (newExpandedGroups.has(groupName)) { - newExpandedGroups.delete(groupName); - } else { - newExpandedGroups.add(groupName); - } - setExpandedGroups(newExpandedGroups); - }, [expandedGroups]); + const toggleExpanded = useCallback( + (testName: string) => { + const newExpanded = new Set(expandedItems); + if (newExpanded.has(testName)) { + newExpanded.delete(testName); + } else { + newExpanded.add(testName); + } + setExpandedItems(newExpanded); + }, + [expandedItems], + ); + + const toggleGroupExpanded = useCallback( + (groupName: string) => { + const newExpandedGroups = new Set(expandedGroups); + if (newExpandedGroups.has(groupName)) { + newExpandedGroups.delete(groupName); + } else { + newExpandedGroups.add(groupName); + } + setExpandedGroups(newExpandedGroups); + }, + [expandedGroups], + ); const updateTest = (name: string, field: keyof Test, value: string) => { setTests( From 8dfc4997e241bc8d6ad93362347edfa8f2dada6f Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 14:03:17 -0700 Subject: [PATCH 17/34] fix linter warnings --- .../ForTests/TestList/TestItemRenderer.tsx | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx index f35014605..6ec33b722 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx @@ -1,5 +1,4 @@ import TestItem from "@pageComponents/DatabasePage/ForTests/TestItem"; -import css from "./index.module.css"; import { Test } from "@gen/graphql-types"; type Props = { @@ -34,26 +33,24 @@ export default function TestItemRenderer({ }: Props) { return ( <> - {tests.map(test => { - return ( - onToggleExpanded(test.testName)} - onUpdateTest={(field, value) => - onUpdateTest(test.testName, field, value) - } - onNameEdit={name => onNameEdit(test.testName, name)} - onNameBlur={() => onNameBlur(test.testName)} - onRunTest={async () => await onRunTest(test.testName)} - onDeleteTest={() => onDeleteTest(test.testName)} - /> - ); - })} + {tests.map(test => ( + onToggleExpanded(test.testName)} + onUpdateTest={(field, value) => + onUpdateTest(test.testName, field, value) + } + onNameEdit={name => onNameEdit(test.testName, name)} + onNameBlur={() => onNameBlur(test.testName)} + onRunTest={async () => await onRunTest(test.testName)} + onDeleteTest={() => onDeleteTest(test.testName)} + /> + ))} ); } From d214da92af9da1d22dc0e836df12437ec9040a70 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 15:25:14 -0700 Subject: [PATCH 18/34] UI improvements --- web/renderer/components/DatabaseNav/index.module.css | 2 +- .../DatabasePage/ForTests/TestGroup/index.module.css | 4 ++-- .../pageComponents/DatabasePage/ForTests/TestGroup/index.tsx | 2 +- .../DatabasePage/ForTests/TestItem/index.module.css | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/web/renderer/components/DatabaseNav/index.module.css b/web/renderer/components/DatabaseNav/index.module.css index 6c9ffe69e..9fd1b7cef 100644 --- a/web/renderer/components/DatabaseNav/index.module.css +++ b/web/renderer/components/DatabaseNav/index.module.css @@ -17,7 +17,7 @@ } li { - @apply min-w-[150px]; + @apply min-w-[130px]; } @screen lg { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css index a9419219e..e46ac201d 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.module.css @@ -40,8 +40,8 @@ } .inlineEditInput { - @apply bg-transparent border border-transparent text-base font-semibold text-storm-500 px-1 py-0.5 m-0 outline-none rounded transition-all; - min-width: 100px; + @apply bg-transparent border border-transparent text-base font-semibold text-storm-500 px-1 py-0.5 mt-0 mb-0 ml-0 mr-3 outline-none rounded transition-all; + min-width: 200px; } .inlineEditInput:hover { diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index df0284b54..26293c0d0 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -113,7 +113,7 @@ export default function TestGroup({ onFocus={() => setIsEditing(true)} onClick={e => e.stopPropagation()} /> - ({testCount} tests) + {`(${testCount} ${testCount === 1 ? 'test' : 'tests'})`}
    {groupResult && ( diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css index 316e6c8c8..a5f70ee8c 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.module.css @@ -1,4 +1,3 @@ -/* TestItem Component Styles */ .item { @apply border-2 border-stone-50 rounded-lg my-2 py-3 px-4 text-sm relative; } @@ -68,7 +67,7 @@ } .separator { - @apply border-t border-stone-300 mb-4 -mx-4; + @apply border-t-2 border-stone-50 mb-4 -mx-4; } .fieldGroup { From 2c325f2e6756e30337e7ab95ceb0c63cf4d091b3 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 15:29:14 -0700 Subject: [PATCH 19/34] decrease button spacing --- .../DatabasePage/ForTests/TestList/index.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css index 53e70f2a7..12fad67b9 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.module.css @@ -119,7 +119,7 @@ } .actionArea { - @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-8 mb-6; + @apply flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2 mb-6; } .createActions { From 69127092e2c0b039da8338cb28d098badd6e42a6 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 15:29:27 -0700 Subject: [PATCH 20/34] run prettier --- .../pageComponents/DatabasePage/ForTests/TestGroup/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index 26293c0d0..37698058a 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -113,7 +113,9 @@ export default function TestGroup({ onFocus={() => setIsEditing(true)} onClick={e => e.stopPropagation()} /> - {`(${testCount} ${testCount === 1 ? 'test' : 'tests'})`} + {`(${testCount} ${testCount === 1 ? "test" : "tests"})`}
    {groupResult && ( From ccb1545bf00d1bdeb15bf3a5466d227008f93c23 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 15:31:31 -0700 Subject: [PATCH 21/34] remove debug logs --- graphql-server/src/tests/test.resolver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/graphql-server/src/tests/test.resolver.ts b/graphql-server/src/tests/test.resolver.ts index 6f8771abf..3ad46e6a7 100644 --- a/graphql-server/src/tests/test.resolver.ts +++ b/graphql-server/src/tests/test.resolver.ts @@ -88,7 +88,6 @@ export class TestResolver { async saveTests(@Args() args: SaveTestsArgs): Promise { const conn = this.conn.connection(); const res = await conn.saveTests(args); - console.dir(res, { depth: null }); return { list: res.generatedMaps.map(t => fromDoltTestRowRes(t)), }; From 4b59d4b0d01c8e396ebb22d84ea876b71ca97012 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 16:07:01 -0700 Subject: [PATCH 22/34] use blur events for updating assertion value --- .../DatabasePage/ForTests/TestItem/index.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx index 05bf0b850..17858fa1d 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx @@ -41,6 +41,7 @@ export default function TestItem({ onDeleteTest, }: Props) { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [localAssertionValue, setLocalAssertionValue] = useState(test.assertionValue); const debounceRef = useRef | null>(null); const debouncedOnUpdateTest = useCallback( @@ -64,6 +65,10 @@ export default function TestItem({ [], ); + useEffect(() => { + setLocalAssertionValue(test.assertionValue); + }, [test.assertionValue]); + const handleDeleteClick = (e: MouseEvent) => { e.stopPropagation(); setShowDeleteConfirm(true); @@ -78,6 +83,12 @@ export default function TestItem({ setShowDeleteConfirm(false); }; + const handleAssertionValueBlur = () => { + if (localAssertionValue !== test.assertionValue) { + onUpdateTest("assertionValue", localAssertionValue); + } + }; + return (
  • onUpdateTest("assertionValue", value)} + value={localAssertionValue} + onChangeString={setLocalAssertionValue} + onBlur={handleAssertionValueBlur} placeholder="Expected result" className={css.fullWidthFormInput} /> From dfbf42e1f413f501d4c07fcc95152affdfde0064 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Thu, 18 Sep 2025 16:08:36 -0700 Subject: [PATCH 23/34] prettier --- .../pageComponents/DatabasePage/ForTests/TestItem/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx index 17858fa1d..7cb309d7a 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx @@ -41,7 +41,9 @@ export default function TestItem({ onDeleteTest, }: Props) { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [localAssertionValue, setLocalAssertionValue] = useState(test.assertionValue); + const [localAssertionValue, setLocalAssertionValue] = useState( + test.assertionValue, + ); const debounceRef = useRef | null>(null); const debouncedOnUpdateTest = useCallback( From cc9be6cb56f66b4f2173b8533fd0bc2480c7dd17 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 22 Sep 2025 13:51:18 -0700 Subject: [PATCH 24/34] fix graphql-server feedback --- graphql-server/schema.gql | 11 +++++-- graphql-server/src/queryFactory/dolt/index.ts | 6 ++-- .../src/queryFactory/dolt/queries.ts | 8 ++--- .../src/queryFactory/doltgres/index.ts | 8 +++-- .../src/queryFactory/doltgres/queries.ts | 9 ++---- graphql-server/src/queryFactory/types.ts | 5 +-- graphql-server/src/tests/test.model.ts | 32 ++++++++++++++++--- graphql-server/src/tests/test.resolver.ts | 19 +++++++---- .../DatabasePage/ForTests/TestList/index.tsx | 2 +- .../ForTests/TestList/useTestList.ts | 17 ++++++---- .../DatabasePage/ForTests/queries.ts | 18 +++++++++-- web/renderer/gen/graphql-types.tsx | 29 ++++++++++++----- 12 files changed, 115 insertions(+), 49 deletions(-) diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index 8b568a9d9..f0ecbd142 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -340,6 +340,9 @@ type TagList { } type Test { + _id: ID! + databaseName: String! + refName: String! testName: String! testGroup: String! testQuery: String! @@ -353,6 +356,9 @@ type TestList { } type TestResult { + _id: ID! + databaseName: String! + refName: String! testName: String! testGroupName: String query: String! @@ -404,7 +410,7 @@ type Query { tags(databaseName: String!): TagList! tag(databaseName: String!, tagName: String!): Tag tests(databaseName: String!, refName: String!): TestList! - runTests(refName: String!, databaseName: String!, identifiers: TestIdentifierArgs): TestResultList! + runTests(refName: String!, databaseName: String!, testIdentifier: TestIdentifierArgs): TestResultList! } enum SortBranchesBy { @@ -433,7 +439,8 @@ enum DiffRowType { } input TestIdentifierArgs { - values: [String!]! + testName: String + groupName: String } type Mutation { diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index 97cbef3bb..7c407a64b 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -595,9 +595,11 @@ export class DoltQueryFactory } async runTests(args: t.RunTestsArgs): t.PR { + const withTestIdentifierArg = args.testIdentifier && (args.testIdentifier.testName !== undefined || args.testIdentifier.groupName !== undefined); + return this.query( - qh.doltTestRun(args.identifiers?.values.length ?? 0), - args.identifiers?.values, + qh.doltTestRun(withTestIdentifierArg), + withTestIdentifierArg ? [args.testIdentifier?.testName ?? args.testIdentifier?.groupName] : undefined, args.databaseName, args.refName, ); diff --git a/graphql-server/src/queryFactory/dolt/queries.ts b/graphql-server/src/queryFactory/dolt/queries.ts index 9c7b1bece..e7edacebf 100644 --- a/graphql-server/src/queryFactory/dolt/queries.ts +++ b/graphql-server/src/queryFactory/dolt/queries.ts @@ -1,5 +1,5 @@ import { SortBranchesBy } from "../../branches/branch.enum"; -import { RawRows } from "../types"; +import { RawRows, TestIdentifierArgs } from "../types"; // TABLE @@ -182,7 +182,5 @@ export const callCheckoutTable = `CALL DOLT_CHECKOUT(?)`; export const callDoltClone = `CALL DOLT_CLONE(?,?)`; -export const doltTestRun = (argCount: number) => { - const placeholders = Array.from({ length: argCount }, () => `?`).join(", "); - return `SELECT * FROM DOLT_TEST_RUN(${placeholders})`; -}; +export const doltTestRun = (withArg?: boolean): string => + `SELECT * FROM DOLT_TEST_RUN(${withArg ? "?" : ""})`; diff --git a/graphql-server/src/queryFactory/doltgres/index.ts b/graphql-server/src/queryFactory/doltgres/index.ts index a47e8495b..4f7a4654a 100644 --- a/graphql-server/src/queryFactory/doltgres/index.ts +++ b/graphql-server/src/queryFactory/doltgres/index.ts @@ -569,12 +569,16 @@ export class DoltgresQueryFactory } async runTests(args: t.RunTestsArgs): t.PR { + const withTestIdentifierArg = args.testIdentifier && (args.testIdentifier.testName !== undefined || args.testIdentifier.groupName !== undefined); + return this.query( - qh.doltTestRun(args.identifiers?.values.length ?? 0), - args.identifiers?.values, + qh.doltTestRun(withTestIdentifierArg), + withTestIdentifierArg ? [args.testIdentifier?.testName ?? args.testIdentifier?.groupName] : undefined, args.databaseName, args.refName, ); + + } async saveTests(args: t.SaveTestsArgs): Promise { diff --git a/graphql-server/src/queryFactory/doltgres/queries.ts b/graphql-server/src/queryFactory/doltgres/queries.ts index c1f56bf9d..52d9dcc9c 100644 --- a/graphql-server/src/queryFactory/doltgres/queries.ts +++ b/graphql-server/src/queryFactory/doltgres/queries.ts @@ -193,10 +193,5 @@ export const callFetchRemote = `SELECT DOLT_FETCH($1::text)`; export const callCreateBranchFromRemote = `CALL DOLT_BRANCH($1::text, $2::text)`; -export const doltTestRun = (argCount: number) => { - const placeholders = Array.from( - { length: argCount }, - (_, i) => `$${i + 1}::text`, - ).join(", "); - return `SELECT * FROM DOLT_TEST_RUN(${placeholders})`; -}; +export const doltTestRun = (withArg?: boolean): string => + `SELECT * FROM DOLT_TEST_RUN(${withArg ? "$1::text" : ""})`; diff --git a/graphql-server/src/queryFactory/types.ts b/graphql-server/src/queryFactory/types.ts index 042007a95..322439c76 100644 --- a/graphql-server/src/queryFactory/types.ts +++ b/graphql-server/src/queryFactory/types.ts @@ -86,9 +86,10 @@ export type SaveTestsArgs = RefArgs & { }; export type TestIdentifierArgs = { - values: string[]; + testName?: string; + groupName?: string; }; export type RunTestsArgs = RefArgs & { - identifiers?: TestIdentifierArgs; + testIdentifier?: TestIdentifierArgs; }; diff --git a/graphql-server/src/tests/test.model.ts b/graphql-server/src/tests/test.model.ts index e60446f32..0e0312480 100644 --- a/graphql-server/src/tests/test.model.ts +++ b/graphql-server/src/tests/test.model.ts @@ -1,9 +1,18 @@ -import { Field, ObjectType } from "@nestjs/graphql"; +import { Field, ID, ObjectType } from "@nestjs/graphql"; import { RawRow } from "../queryFactory/types"; import { ObjectLiteral } from "typeorm"; @ObjectType() export class Test { + @Field(_type => ID) + _id: string; + + @Field() + databaseName: string; + + @Field() + refName: string; + @Field() testName: string; @@ -31,11 +40,20 @@ export class TestList { @ObjectType() export class TestResult { + @Field(_type => ID) + _id: string; + + @Field() + databaseName: string; + + @Field() + refName: string; + @Field() testName: string; @Field({ nullable: true }) - testGroupName: string; + testGroupName?: string; @Field() query: string; @@ -53,8 +71,11 @@ export class TestResultList { list: TestResult[]; } -export function fromDoltTestRowRes(test: RawRow | ObjectLiteral): Test { +export function fromDoltTestRowRes(databaseName: string, refName: string, test: RawRow | ObjectLiteral): Test { return { + _id: `databases/${databaseName}/refs/${refName}/tests/${test.test_name}`, + databaseName: databaseName, + refName: refName, testName: test.test_name, testGroup: test.test_group, testQuery: test.test_query, @@ -64,8 +85,11 @@ export function fromDoltTestRowRes(test: RawRow | ObjectLiteral): Test { }; } -export function fromDoltTestResultRowRes(testResult: RawRow): TestResult { +export function fromDoltTestResultRowRes(databaseName: string, refName: string, testResult: RawRow): TestResult { return { + _id: `databases/${databaseName}/refs/${refName}/testResults/${testResult.test_name}`, + databaseName: databaseName, + refName: refName, testName: testResult.test_name, testGroupName: testResult.test_group_name, query: testResult.query, diff --git a/graphql-server/src/tests/test.resolver.ts b/graphql-server/src/tests/test.resolver.ts index 3ad46e6a7..a6041a1ee 100644 --- a/graphql-server/src/tests/test.resolver.ts +++ b/graphql-server/src/tests/test.resolver.ts @@ -19,6 +19,8 @@ import { @InputType() class TestArgs { + + @Field() @Field() testName: string; @@ -52,14 +54,17 @@ class SaveTestsArgs extends RefArgs { @InputType() class TestIdentifierArgs { - @Field(_type => [String]) - values: string[]; + @Field({ nullable: true }) + testName?: string; + + @Field({ nullable: true }) + groupName?: string; } @ArgsType() class RunTestsArgs extends RefArgs { @Field({ nullable: true }) - identifiers?: TestIdentifierArgs; + testIdentifier?: TestIdentifierArgs; } @Resolver(_of => Test) @@ -71,16 +76,16 @@ export class TestResolver { const conn = this.conn.connection(); const res = await conn.getTests(args); return { - list: res.map(t => fromDoltTestRowRes(t)), + list: res.map(t => fromDoltTestRowRes(args.databaseName, args.refName, t)), }; } @Query(_returns => TestResultList) - async runTests(@Args() args: RunTestsArgs) { + async runTests(@Args() args: RunTestsArgs): Promise { const conn = this.conn.connection(); const res = await conn.runTests(args); return { - list: res.map(t => fromDoltTestResultRowRes(t)), + list: res.map(t => fromDoltTestResultRowRes(args.databaseName, args.refName, t)), }; } @@ -89,7 +94,7 @@ export class TestResolver { const conn = this.conn.connection(); const res = await conn.saveTests(args); return { - list: res.generatedMaps.map(t => fromDoltTestRowRes(t)), + list: res.generatedMaps.map(t => fromDoltTestRowRes(args.databaseName, args.refName, t)), }; } } diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx index b70cac562..d8973c143 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/index.tsx @@ -109,7 +109,7 @@ export default function TestList({ params }: Props) { testCount={groupTests.length} groupResult={getGroupResult(groupName)} onRunGroup={async () => await handleRunGroup(groupName)} - onDeleteGroup={async () => handleDeleteGroup(groupName)} + onDeleteGroup={() => handleDeleteGroup(groupName)} onRenameGroup={handleRenameGroup} onCreateTest={group => handleCreateTest(group)} /> diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts index 08e83ab93..d62a46d08 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts @@ -36,7 +36,7 @@ export function useTestList(params: RefParams) { variables: { databaseName: params.databaseName, refName: params.refName, - tests: { list: tests }, + tests: { list: tests.map(({ _id, databaseName: _databaseName, refName: _refName, ...test }) => test) }, }, }); @@ -165,9 +165,9 @@ export function useTestList(params: RefParams) { variables: { databaseName: params.databaseName, refName: params.refName, - identifiers: { - values: [testName], - }, + testIdentifier: { + testName, + } }, }); @@ -192,9 +192,9 @@ export function useTestList(params: RefParams) { variables: { databaseName: params.databaseName, refName: params.refName, - identifiers: { - values: [groupName], - }, + testIdentifier: { + groupName, + } }, }); result.data?.runTests.list.map(test => @@ -302,6 +302,9 @@ export function useTestList(params: RefParams) { assertionComparator: "==", assertionValue: "", testGroup: groupName ?? "", + _id: "", + databaseName: params.databaseName, + refName: params.refName, }; setTests([...tests, newTest]); diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts index 742ee9147..e2ea15785 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/queries.ts @@ -4,6 +4,9 @@ export const LIST_TESTS = gql` query TestList($databaseName: String!, $refName: String!) { tests(databaseName: $databaseName, refName: $refName) { list { + _id + databaseName + refName testName testQuery testGroup @@ -16,6 +19,14 @@ export const LIST_TESTS = gql` `; export const SAVE_TESTS = gql` + fragment TestFragment on Test { + testName + testGroup + testQuery + assertionType + assertionComparator + assertionValue + } mutation SaveTests( $databaseName: String! $refName: String! @@ -38,14 +49,17 @@ export const RUN_TESTS = gql` query RunTests( $databaseName: String! $refName: String! - $identifiers: TestIdentifierArgs + $testIdentifier: TestIdentifierArgs ) { runTests( databaseName: $databaseName refName: $refName - identifiers: $identifiers + testIdentifier: $testIdentifier ) { list { + _id + databaseName + refName testName testGroupName query diff --git a/web/renderer/gen/graphql-types.tsx b/web/renderer/gen/graphql-types.tsx index ebd7a3bb5..e99d16651 100644 --- a/web/renderer/gen/graphql-types.tsx +++ b/web/renderer/gen/graphql-types.tsx @@ -694,8 +694,8 @@ export type QueryRowsArgs = { export type QueryRunTestsArgs = { databaseName: Scalars['String']['input']; - identifiers?: InputMaybe; refName: Scalars['String']['input']; + testIdentifier?: InputMaybe; }; @@ -939,9 +939,12 @@ export type TagList = { export type Test = { __typename?: 'Test'; + _id: Scalars['ID']['output']; assertionComparator: Scalars['String']['output']; assertionType: Scalars['String']['output']; assertionValue: Scalars['String']['output']; + databaseName: Scalars['String']['output']; + refName: Scalars['String']['output']; testGroup: Scalars['String']['output']; testName: Scalars['String']['output']; testQuery: Scalars['String']['output']; @@ -957,7 +960,8 @@ export type TestArgs = { }; export type TestIdentifierArgs = { - values: Array; + groupName?: InputMaybe; + testName?: InputMaybe; }; export type TestList = { @@ -971,8 +975,11 @@ export type TestListArgs = { export type TestResult = { __typename?: 'TestResult'; + _id: Scalars['ID']['output']; + databaseName: Scalars['String']['output']; message: Scalars['String']['output']; query: Scalars['String']['output']; + refName: Scalars['String']['output']; status: Scalars['String']['output']; testGroupName?: Maybe; testName: Scalars['String']['output']; @@ -1584,7 +1591,7 @@ export type TestListQueryVariables = Exact<{ }>; -export type TestListQuery = { __typename?: 'Query', tests: { __typename?: 'TestList', list: Array<{ __typename?: 'Test', testName: string, testQuery: string, testGroup: string, assertionType: string, assertionComparator: string, assertionValue: string }> } }; +export type TestListQuery = { __typename?: 'Query', tests: { __typename?: 'TestList', list: Array<{ __typename?: 'Test', _id: string, databaseName: string, refName: string, testName: string, testQuery: string, testGroup: string, assertionType: string, assertionComparator: string, assertionValue: string }> } }; export type SaveTestsMutationVariables = Exact<{ databaseName: Scalars['String']['input']; @@ -1598,11 +1605,11 @@ export type SaveTestsMutation = { __typename?: 'Mutation', saveTests: { __typena export type RunTestsQueryVariables = Exact<{ databaseName: Scalars['String']['input']; refName: Scalars['String']['input']; - identifiers?: InputMaybe; + testIdentifier?: InputMaybe; }>; -export type RunTestsQuery = { __typename?: 'Query', runTests: { __typename?: 'TestResultList', list: Array<{ __typename?: 'TestResult', testName: string, testGroupName?: string | null, query: string, status: string, message: string }> } }; +export type RunTestsQuery = { __typename?: 'Query', runTests: { __typename?: 'TestResultList', list: Array<{ __typename?: 'TestResult', _id: string, databaseName: string, refName: string, testName: string, testGroupName?: string | null, query: string, status: string, message: string }> } }; export type LoadDataMutationVariables = Exact<{ databaseName: Scalars['String']['input']; @@ -4561,6 +4568,9 @@ export const TestListDocument = gql` query TestList($databaseName: String!, $refName: String!) { tests(databaseName: $databaseName, refName: $refName) { list { + _id + databaseName + refName testName testQuery testGroup @@ -4648,13 +4658,16 @@ export type SaveTestsMutationHookResult = ReturnType; export type SaveTestsMutationOptions = Apollo.BaseMutationOptions; export const RunTestsDocument = gql` - query RunTests($databaseName: String!, $refName: String!, $identifiers: TestIdentifierArgs) { + query RunTests($databaseName: String!, $refName: String!, $testIdentifier: TestIdentifierArgs) { runTests( databaseName: $databaseName refName: $refName - identifiers: $identifiers + testIdentifier: $testIdentifier ) { list { + _id + databaseName + refName testName testGroupName query @@ -4679,7 +4692,7 @@ export const RunTestsDocument = gql` * variables: { * databaseName: // value for 'databaseName' * refName: // value for 'refName' - * identifiers: // value for 'identifiers' + * testIdentifier: // value for 'testIdentifier' * }, * }); */ From f544e7ccb62dd3b6c6c06eb55c864c0d021666f8 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Mon, 22 Sep 2025 22:20:40 -0700 Subject: [PATCH 25/34] use context instead of a massive hook, refactor some event handlers --- .../TestResultsListItem/index.module.css | 58 -- .../TestResultsListItem/index.tsx | 65 --- .../TestResultsForMergeList/index.module.css | 107 ---- .../TestResultsForMergeList/index.tsx | 173 ------ .../PullActions/Merge/index.module.css | 155 ++++++ .../ForPulls/PullActions/Merge/index.tsx | 227 +++++++- .../DatabasePage/ForTests/TestGroup/index.tsx | 93 ++-- .../DatabasePage/ForTests/TestItem/index.tsx | 113 ++-- .../ForTests/TestList/CreateDropdown.tsx | 7 +- .../ForTests/TestList/TestItemRenderer.tsx | 56 -- .../DatabasePage/ForTests/TestList/index.tsx | 92 ++-- .../ForTests/TestList/useTestList.ts | 504 ------------------ .../DatabasePage/ForTests/context/index.tsx | 375 +++++++++++++ .../DatabasePage/ForTests/context/state.ts | 33 ++ .../DatabasePage/ForTests/index.tsx | 5 +- .../DatabasePage/ForTests/utils.ts | 78 +++ 16 files changed, 1041 insertions(+), 1100 deletions(-) delete mode 100644 web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css delete mode 100644 web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx delete mode 100644 web/renderer/components/TestResultsForMergeList/index.module.css delete mode 100644 web/renderer/components/TestResultsForMergeList/index.tsx delete mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/TestItemRenderer.tsx delete mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/useTestList.ts create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/context/state.ts create mode 100644 web/renderer/components/pageComponents/DatabasePage/ForTests/utils.ts diff --git a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css deleted file mode 100644 index 3fd325454..000000000 --- a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.module.css +++ /dev/null @@ -1,58 +0,0 @@ -.itemContainer { - @apply border-b border-stone-300 py-3 px-4 flex items-start gap-3 hover:bg-stone-50 transition-colors; -} - -li:last-child .itemContainer { - @apply border-b-0; -} - -.icon { - @apply flex-shrink-0 mt-0.5; - svg { - @apply w-4 h-4; - } -} - -.successIcon { - @apply text-green-500; -} - -.failureIcon { - @apply text-red-400; -} - -.pendingIcon { - @apply text-orange-500; -} - -.content { - @apply flex-1 min-w-0; -} - -.testTitle { - @apply flex flex-col gap-1; -} - -.testName { - @apply font-medium text-stone-700 text-sm; -} - -.testMessage { - @apply text-xs text-stone-600 break-words; -} - -.red { - @apply bg-coral-50 border-red-300; -} - -.green { - @apply bg-green-50 border-green-300; -} - -.orange { - @apply bg-orange-50 border-orange-200; -} - -.linkContent { - @apply flex items-start gap-3 w-full no-underline; -} diff --git a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx b/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx deleted file mode 100644 index 4e0bc892c..000000000 --- a/web/renderer/components/TestResultsForMergeList/TestResultsListItem/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { TestResult } from "@gen/graphql-types"; -import { RefParams } from "@lib/params"; -import cx from "classnames"; -import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; -import { FiX } from "@react-icons/all-files/fi/FiX"; -import { excerpt } from "@dolthub/web-utils"; -import css from "./index.module.css"; -import Link from "@components/links/Link"; -import { tests as testsUrl } from "@lib/urls"; - -export type TestStatusColors = { - red: boolean; - orange: boolean; - green: boolean; -}; - -type Props = { - test: TestResult; - params: RefParams; -}; - -function TestTitle({ test }: { test: TestResult }) { - return ( -
    - {excerpt(test.testName, 50)} - {test.message && ( - {excerpt(test.message, 100)} - )} -
    - ); -} - -function IconSwitch({ test }: { test: TestResult }) { - if (test.status === "PASS") { - return ; - } - return ; -} - -export default function TestResultsListItem({ test, params }: Props) { - const isSuccess = test.status === "PASS"; - const isFailure = !isSuccess; - - return ( -
  • - -
    - -
    -
    - -
    - -
  • - ); -} diff --git a/web/renderer/components/TestResultsForMergeList/index.module.css b/web/renderer/components/TestResultsForMergeList/index.module.css deleted file mode 100644 index 4f7cbed12..000000000 --- a/web/renderer/components/TestResultsForMergeList/index.module.css +++ /dev/null @@ -1,107 +0,0 @@ -.outer { - @apply mx-auto max-w-3xl flex flex-col mb-4 mt-4; - - h3 { - @apply text-center mt-4 mb-4; - } - - @screen lg { - @apply flex-row; - } -} - -.container { - @apply w-full mb-0 rounded-xl min-w-[15rem]; - @screen lg { - @apply w-[calc(100%-4rem)] mx-3 mb-0; - } -} - -.top { - @apply bg-green-50 pl-4 pr-4 py-2 border border-green-500 flex justify-between items-start rounded-t-xl flex-wrap w-full; - - @screen lg { - @apply items-center pl-6; - } -} - -.picContainer { - @apply w-7 h-7 mb-4 mr-1 text-white relative rounded-full bg-green-400 flex-shrink-0; - - @screen lg { - @apply mb-0 mt-3 ml-2; - } - svg { - @apply text-white mt-1 ml-1 w-5 h-5; - } -} - -.redIcon { - @apply bg-red-300; -} - -.orangeIcon { - @apply bg-orange-300; -} - -.tests { - @apply border-x border-b rounded-b-xl bg-white border-green-500; - li:last-child { - @apply rounded-b-xl overflow-hidden; - } -} - -.green { - @apply text-green-500; -} - -.testsStatusIcon { - @apply inline-block mr-2 text-2xl; -} - -.red { - @apply text-red-400 flex border-red-400 bg-coral-50; -} - -.testsRed { - @apply border-red-400; -} - -.orange { - @apply text-orange-500 flex border-orange-300 bg-orange-50; -} - -.testsOrange { - @apply border-orange-300; -} - -.testsGreen { - @apply border-green-500; -} - -.noTests { - @apply py-4 px-6 text-center text-stone-500 text-sm; -} - -.runButton { - @apply mx-2; - @screen lg { - @apply mx-0; - } -} - -.titleSection { - @apply flex items-center; -} - -.greenButton { - @apply bg-green-500 text-white border-green-500; -} - -.redButton { - @apply bg-red-500 text-white border-red-500; -} - -.orangeButton { - @apply bg-orange-500 text-white border-orange-500; -} diff --git a/web/renderer/components/TestResultsForMergeList/index.tsx b/web/renderer/components/TestResultsForMergeList/index.tsx deleted file mode 100644 index 08a65ce4b..000000000 --- a/web/renderer/components/TestResultsForMergeList/index.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { - TestResult, - useRunTestsLazyQuery, - useTestListQuery, -} from "@gen/graphql-types"; -import { RefParams } from "@lib/params"; -import { Button } from "@dolthub/react-components"; -import cx from "classnames"; -import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; -import { FiX } from "@react-icons/all-files/fi/FiX"; -import { FiCircle } from "@react-icons/all-files/fi/FiCircle"; -import TestResultsListItem, { TestStatusColors } from "./TestResultsListItem"; -import css from "./index.module.css"; -import { useCallback, useState } from "react"; -import { Arrow } from "@pageComponents/DatabasePage/ForPulls/PullActions/Merge/Arrow"; - -type Props = { - params: RefParams; -}; - -function TestResultsTitle({ - red, - green, - orange, - onRunTests, -}: TestStatusColors & { onRunTests: () => void }) { - return ( - <> -
    - - Tests -
    - - - ); -} - -function IconSwitch({ - red, - green, - orange, - className, -}: TestStatusColors & { className?: string }) { - if (red) return ; - if (orange) return ; - if (green) return ; - return null; -} - -export default function TestResultsForMergeList({ params }: Props) { - const [testResults, setTestResults] = useState([]); - const [runTests] = useRunTestsLazyQuery(); - const { data } = useTestListQuery({ - variables: { - databaseName: params.databaseName, - refName: params.refName, - }, - }); - const handleRunTests = useCallback(async () => { - try { - const result = await runTests({ - variables: { - databaseName: params.databaseName, - refName: params.refName, - }, - }); - - setTestResults(result.data?.runTests.list ?? []); - } catch { - setTestResults([]); - } - }, [runTests, params.databaseName, params.refName]); - - if (!data?.tests.list || data.tests.list.length === 0) { - return null; - } - - const { red, orange, green } = getTestStatusColors(testResults); - - return ( -
    - - - - -
    -
    - -
    -
    -
      - {testResults.length > 0 ? ( - testResults.map(test => ( - - )) - ) : ( -
    • - Run tests to see results here. -
    • - )} -
    -
    -
    -
    - ); -} - -function getTestStatusColors(tests: TestResult[]): TestStatusColors { - if (tests.length === 0) { - return { - red: false, - green: false, - orange: true, - }; - } - - const failedTests = tests.filter(t => t.status !== "PASS"); - - if (failedTests.length > 0) { - return { - red: true, - green: false, - orange: false, - }; - } - - return { - red: false, - green: true, - orange: false, - }; -} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css index 824a0abbd..48c5f7276 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css +++ b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css @@ -273,3 +273,158 @@ @apply rounded-b-lg; } } + +.testResultsOuter { + @apply mx-auto max-w-3xl flex flex-col mb-4 mt-4; + + h3 { + @apply text-center mt-4 mb-4; + } + + @screen lg { + @apply flex-row; + } +} + +.testResultsContainer { + @apply w-full mb-0 rounded-xl min-w-[15rem]; + @screen lg { + @apply w-[calc(100%-4rem)] mx-3 mb-0; + } +} + +.testResultsTop { + @apply bg-green-50 pl-4 pr-4 py-2 border border-green-500 flex justify-between items-start rounded-t-xl flex-wrap w-full; + + @screen lg { + @apply items-center pl-6; + } +} + +.testResultsPicContainer { + @apply w-7 h-7 mb-4 mr-1 text-white relative rounded-full bg-green-400 flex-shrink-0; + + @screen lg { + @apply mb-0 mt-3 ml-2; + } + svg { + @apply text-white mt-1 ml-1 w-5 h-5; + } +} + +.testResultsRedIcon { + @apply bg-red-300; +} + +.testResultsOrangeIcon { + @apply bg-orange-300; +} + +.testResultsTests { + @apply border-x border-b rounded-b-xl bg-white border-green-500; + li:last-child { + @apply rounded-b-xl overflow-hidden; + } +} + +.testResultsGreen { + @apply text-green-500; +} + +.testResultsTestsStatusIcon { + @apply inline-block mr-2 text-2xl; +} + +.testResultsRed { + @apply text-red-400 flex border-red-400 bg-coral-50; +} + +.testResultsTestsRed { + @apply border-red-400; +} + +.testResultsOrange { + @apply text-orange-500 flex border-orange-300 bg-orange-50; +} + +.testResultsTestsOrange { + @apply border-orange-300; +} + +.testResultsTestsGreen { + @apply border-green-500; +} + +.testResultsNoTests { + @apply py-4 px-6 text-center text-stone-500 text-sm; +} + +.testResultsRunButton { + @apply mx-2; + @screen lg { + @apply mx-0; + } +} + +.testResultsTitleSection { + @apply flex items-center; +} + +.testResultsGreenButton { + @apply bg-green-500 text-white border-green-500; +} + +.testResultsRedButton { + @apply bg-red-500 text-white border-red-500; +} + +.testResultsOrangeButton { + @apply bg-orange-500 text-white border-orange-500; +} + +.testResultsItemContainer { + @apply border-b border-stone-300 py-3 px-4 flex items-start gap-3 hover:bg-stone-50 transition-colors; +} + +li:last-child .testResultsItemContainer { + @apply border-b-0; +} + +.testResultsIcon { + @apply flex-shrink-0 mt-0.5; + svg { + @apply w-4 h-4; + } +} + +.testResultsSuccessIcon { + @apply text-green-500; +} + +.testResultsFailureIcon { + @apply text-red-400; +} + +.testResultsPendingIcon { + @apply text-orange-500; +} + +.testResultsContent { + @apply flex-1 min-w-0; +} + +.testResultsTestTitle { + @apply flex flex-col gap-1; +} + +.testResultsTestName { + @apply font-medium text-stone-700 text-sm; +} + +.testResultsTestMessage { + @apply text-xs text-stone-600 break-words; +} + +.testResultsLinkContent { + @apply flex items-start gap-3 w-full no-underline; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx index 31cc1cc6e..4fd18e19a 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.tsx @@ -1,12 +1,23 @@ +import { useCallback, useState } from "react"; import HeaderUserCheckbox from "@components/HeaderUserCheckbox"; import { Button, SmallLoader } from "@dolthub/react-components"; -import { PullDetailsFragment } from "@gen/graphql-types"; +import { + PullDetailsFragment, + TestResult, + useRunTestsLazyQuery, + useTestListQuery, +} from "@gen/graphql-types"; import useDatabaseDetails from "@hooks/useDatabaseDetails"; import { ApolloErrorType } from "@lib/errors/types"; -import { PullDiffParams } from "@lib/params"; +import { PullDiffParams, RefParams } from "@lib/params"; import { FiGitPullRequest } from "@react-icons/all-files/fi/FiGitPullRequest"; +import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; +import { FiX } from "@react-icons/all-files/fi/FiX"; +import { FiCircle } from "@react-icons/all-files/fi/FiCircle"; +import { excerpt } from "@dolthub/web-utils"; +import Link from "@components/links/Link"; +import { tests as testsUrl } from "@lib/urls"; import cx from "classnames"; -import { useState } from "react"; import { Arrow } from "./Arrow"; import ErrorsWithDirections from "./ErrorsWithDirections"; import MergeConflictsDirections from "./ErrorsWithDirections/MergeConflictsDirections"; @@ -15,7 +26,7 @@ import MergeMessageTitle from "./MergeMessageTitle"; import ResolveModal from "./ResolveModal"; import css from "./index.module.css"; import useMergeButton, { MergeButtonState } from "./useMergeButton"; -import TestResultsForMergeList from "@components/TestResultsForMergeList"; + type Props = { params: PullDiffParams; @@ -41,7 +52,7 @@ export default function Merge(props: Props) { return (
    - +
    @@ -153,3 +164,209 @@ function MergeButton(props: MergeButtonProps) {
    ); } + +type TestStatusColors = { + red: boolean; + orange: boolean; + green: boolean; +}; + +function TestResultsTitle({ + red, + green, + orange, + onRunTests, + }: TestStatusColors & { onRunTests: () => void }) { + return ( + <> +
    + + Tests +
    + + + ); +} + +function TestResultsIconSwitch({ + red, + green, + orange, + className, + }: TestStatusColors & { className?: string }) { + if (red) return ; + if (orange) return ; + if (green) return ; + return null; +} + +function TestTitle({ test }: { test: TestResult }) { + return ( +
    + {excerpt(test.testName, 50)} + {test.message && ( + {excerpt(test.message, 100)} + )} +
    + ); +} + +function TestResultsListItemIconSwitch({ test }: { test: TestResult }) { + if (test.status === "PASS") { + return ; + } + return ; +} + +function TestResultsListItem({ test, params }: { test: TestResult; params: RefParams }) { + const isSuccess = test.status === "PASS"; + const isFailure = !isSuccess; + + return ( +
  • + +
    + +
    +
    + +
    + +
  • + ); +} + +function TestResults({ params }: { params: RefParams }) { + const [testResults, setTestResults] = useState([]); + const [runTests] = useRunTestsLazyQuery(); + const { data } = useTestListQuery({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + }, + }); + + const handleRunTests = useCallback(async () => { + try { + const result = await runTests({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + }, + }); + + setTestResults(result.data?.runTests.list ?? []); + } catch { + setTestResults([]); + } + }, [runTests, params.databaseName, params.refName]); + + if (!data?.tests.list || data.tests.list.length === 0) { + return null; + } + + const { red, orange, green } = getTestStatusColors(testResults); + + return ( +
    + + + + +
    +
    + +
    +
    +
      + {testResults.length > 0 ? ( + testResults.map(test => ( + + )) + ) : ( +
    • + Run tests to see results here. +
    • + )} +
    +
    +
    +
    + ); +} + +function getTestStatusColors(tests: TestResult[]): TestStatusColors { + if (tests.length === 0) { + return { + red: false, + green: false, + orange: true, + }; + } + + const failedTests = tests.filter(t => t.status !== "PASS"); + + if (failedTests.length > 0) { + return { + red: true, + green: false, + orange: false, + }; + } + + return { + red: false, + green: true, + orange: false, + }; +} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index 37698058a..ddb71f77c 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -9,40 +9,76 @@ import { useState, KeyboardEvent, ChangeEvent, MouseEvent } from "react"; import cx from "classnames"; import css from "./index.module.css"; import ConfirmationModal from "../ConfirmationModal"; +import { pluralize } from "@dolthub/web-utils"; +import { useTestContext } from "../context"; +import { getGroupResult } from "@pageComponents/DatabasePage/ForTests/utils"; type Props = { group: string; - isExpanded: boolean; - onToggle: () => void; - testCount: number; className?: string; - groupResult?: "passed" | "failed"; - onRunGroup: () => void; - onDeleteGroup: () => void; - onRenameGroup?: (oldName: string, newName: string) => void; - onCreateTest: (groupName: string) => void; }; export default function TestGroup({ group, - isExpanded, - onToggle, - testCount, className, - groupResult, - onRunGroup, - onDeleteGroup, - onRenameGroup, - onCreateTest, }: Props) { + const { + tests, + expandedGroups, + groupedTests, + testResults, + emptyGroups, + toggleGroupExpanded, + handleRunGroup, + setState, + handleCreateTest, + } = useTestContext(); + const groupName = group || "No Group"; const [localGroupName, setLocalGroupName] = useState(groupName); const [isEditing, setIsEditing] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const handleRunGroup = (e: MouseEvent) => { + const isExpanded = expandedGroups.has(group); + const testCount = groupedTests[group].length || 0; + const groupResult = getGroupResult(group, groupedTests, testResults); + + const handleRunGroupClick = async (e: MouseEvent) => { e.stopPropagation(); - onRunGroup(); + await handleRunGroup(group); + }; + + const handleDeleteGroup = (groupName: string) => { + const newExpandedGroups = new Set(expandedGroups); + newExpandedGroups.delete(groupName); + const newEmptyGroups = new Set(emptyGroups); + newEmptyGroups.delete(groupName); + setState({ + tests: tests.filter(test => test.testGroup !== groupName), + expandedGroups: newExpandedGroups, + emptyGroups: newEmptyGroups, + hasUnsavedChanges: true, + }); + setShowDeleteConfirm(false); + } + + const handleRenameGroup = (oldName: string, newName: string) => { + if (newName.trim() && oldName !== newName.trim()) { + const newExpandedGroups = new Set(expandedGroups); + if (newExpandedGroups.has(oldName)) { + newExpandedGroups.delete(oldName); + newExpandedGroups.add(newName.trim()); + } + setState({ + tests: tests.map(test => + test.testGroup === oldName + ? { ...test, testGroup: newName.trim() } + : test, + ), + expandedGroups: newExpandedGroups, + hasUnsavedChanges: true, + }); + } }; const handleDeleteClick = (e: MouseEvent) => { @@ -50,14 +86,9 @@ export default function TestGroup({ setShowDeleteConfirm(true); }; - const handleCreateTest = (e: MouseEvent) => { + const handleCreateTestClick = (e: MouseEvent) => { e.stopPropagation(); - onCreateTest(groupName); - }; - - const handleConfirmDelete = () => { - setShowDeleteConfirm(false); - onDeleteGroup(); + handleCreateTest(group); }; const handleCancelDelete = () => { @@ -70,7 +101,7 @@ export default function TestGroup({ const handleInputBlur = () => { if (localGroupName.trim() && localGroupName !== groupName) { - onRenameGroup?.(groupName, localGroupName.trim()); + handleRenameGroup(groupName, localGroupName.trim()); } else { setLocalGroupName(groupName); } @@ -88,7 +119,7 @@ export default function TestGroup({ const handleHeaderClick = () => { if (!isEditing) { - onToggle(); + toggleGroupExpanded(group); } }; @@ -115,7 +146,7 @@ export default function TestGroup({ /> {`(${testCount} ${testCount === 1 ? "test" : "tests"})`} + >{`${testCount} ${pluralize(testCount, "test")}`}
    {groupResult && ( @@ -141,7 +172,7 @@ export default function TestGroup({
    )} @@ -172,7 +203,7 @@ export default function TestGroup({ title="Delete Test Group" message={`Are you sure you want to delete the "${groupName}" test group? This will delete ${testCount} test${testCount !== 1 ? "s" : ""} in this group.`} confirmText="Delete Group" - onConfirm={handleConfirmDelete} + onConfirm={() => handleDeleteGroup(groupName)} onCancel={handleCancelDelete} destructive={true} /> diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx index 7cb309d7a..99c823ed4 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestItem/index.tsx @@ -7,55 +7,92 @@ import { FaTimes } from "@react-icons/all-files/fa/FaTimes"; import cx from "classnames"; import css from "./index.module.css"; import QueryEditor from "../QueryEditor"; -import { MouseEvent, useState, useCallback, useRef, useEffect } from "react"; -import { Test } from "@gen/graphql-types"; +import { MouseEvent, useState, useRef, useEffect, useCallback } from "react"; import ConfirmationModal from "@pageComponents/DatabasePage/ForTests/ConfirmationModal"; +import { useTestContext } from "../context"; +import { Test } from "@gen/graphql-types"; type Props = { test: Test; - groupOptions: string[]; - isExpanded: boolean; - editingName: string | undefined; - testResult?: { status: "passed" | "failed"; error?: string }; className?: string; - onToggleExpanded: () => void; - onUpdateTest: (field: keyof Test, value: string) => void; - onNameEdit: (name: string) => void; - onNameBlur: () => void; - onRunTest: () => void; - onDeleteTest: () => void; }; export default function TestItem({ test, - groupOptions, - isExpanded, - editingName, - testResult, className, - onToggleExpanded, - onUpdateTest, - onNameEdit, - onNameBlur, - onRunTest, - onDeleteTest, }: Props) { + const { + expandedItems, + editingTestNames, + testResults, + sortedGroupEntries, + tests, + toggleExpanded, + handleRunTest, + setState, + } = useTestContext(); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [localAssertionValue, setLocalAssertionValue] = useState( test.assertionValue, ); const debounceRef = useRef | null>(null); - const debouncedOnUpdateTest = useCallback( + const updateTest = useCallback((name: string, field: keyof Test, value: string) => { + setState({ + tests: tests.map((test: Test) => + test.testName === name ? { ...test, [field]: value } : test, + ), + hasUnsavedChanges: true, + }); + }, [tests, setState]); + + const handleDeleteTest = (testName: string) => { + const newExpandedItems = new Set(expandedItems); + newExpandedItems.delete(testName); + setState({ + tests: tests.filter(test => test.testName !== testName), + expandedItems: newExpandedItems, + hasUnsavedChanges: true, + }); + }; + + const handleTestNameEdit = (testId: string, name: string) => { + setState({ + editingTestNames: { + ...editingTestNames, + [testId]: name, + }, + }); + }; + + const handleTestNameBlur = (testName: string) => { + const newName = editingTestNames[testName]; + const test = tests.find(t => t.testName === testName); + if (newName.trim() && newName !== test?.testName) { + updateTest(testName, "testName", newName.trim()); + } + const newEditingTestNames = { ...editingTestNames }; + delete newEditingTestNames[testName]; + setState({ editingTestNames: newEditingTestNames }); + } + + const groupOptions = sortedGroupEntries + .map(entry => entry[0]) + .filter(group => group !== ""); + const isExpanded = expandedItems.has(test.testName); + const editingName = editingTestNames[test.testName]; + const testResult = testResults[test.testName]; + + const debouncedOnUpdateTest = ( (field: keyof Test, value: string) => { if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { - onUpdateTest(field, value); + updateTest(test.testName, field, value); }, 500); // 500ms debounce - }, - [onUpdateTest], + } ); useEffect( @@ -78,7 +115,7 @@ export default function TestItem({ const handleConfirmDelete = () => { setShowDeleteConfirm(false); - onDeleteTest(); + handleDeleteTest(test.testName); }; const handleCancelDelete = () => { @@ -87,7 +124,7 @@ export default function TestItem({ const handleAssertionValueBlur = () => { if (localAssertionValue !== test.assertionValue) { - onUpdateTest("assertionValue", localAssertionValue); + updateTest(test.testName, "assertionValue", localAssertionValue); } }; @@ -101,15 +138,15 @@ export default function TestItem({ )} data-test-name={test.testName} > -
    +
    toggleExpanded(test.testName)}>
    onNameEdit(e.target.value)} - onFocus={() => onNameEdit(test.testName)} - onBlur={onNameBlur} + value={editingName || test.testName} + onChange={e => handleTestNameEdit(test.testName, e.target.value)} + onFocus={() => handleTestNameEdit(test.testName, test.testName)} + onBlur={() => handleTestNameBlur(test.testName)} onClick={e => e.stopPropagation()} placeholder="Test name" /> @@ -138,9 +175,9 @@ export default function TestItem({ )}
    { + onClick={async (e: MouseEvent) => { e.stopPropagation(); - onRunTest(); + await handleRunTest(test.testName); }} className={cx(css.testActionBtn, css.runBtn)} data-tooltip-content="Run test" @@ -180,7 +217,7 @@ export default function TestItem({ }), ]} val={test.testGroup || ""} - onChangeValue={value => onUpdateTest("testGroup", value || "")} + onChangeValue={value => updateTest(test.testName, "testGroup", value || "")} />
    @@ -204,7 +241,7 @@ export default function TestItem({ ]} val={test.assertionType} onChangeValue={value => - onUpdateTest("assertionType", value || "") + updateTest(test.testName, "assertionType", value || "") } />
    @@ -221,7 +258,7 @@ export default function TestItem({ ]} val={test.assertionComparator} onChangeValue={value => - onUpdateTest("assertionComparator", value || "") + updateTest(test.testName, "assertionComparator", value || "") } />
    diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx index 33ff98810..c596fe4e8 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestList/CreateDropdown.tsx @@ -6,13 +6,14 @@ import { FaFile } from "@react-icons/all-files/fa/FaFile"; import { FaFolder } from "@react-icons/all-files/fa/FaFolder"; import { FiPlus } from "@react-icons/all-files/fi/FiPlus"; import css from "./index.module.css"; +import { useTestContext } from "../context"; type Props = { - onCreateTest: () => void; onCreateGroup: () => void; }; -export default function CreateDropdown({ onCreateTest, onCreateGroup }: Props) { +export default function CreateDropdown({ onCreateGroup }: Props) { + const { handleCreateTest } = useTestContext(); return ( - -
    + {!testsLoading && !testsError && ( +
    +
    + + setShowNewGroupModal(true)} + /> + + + + +
    -
    - {tests.length > 0 && ( - - )} +
    + {tests.length > 0 && ( + + )} +
    -
    + )}
    + ) : testsError ? ( +
    +

    Failed to load tests: {testsError}

    +
    + ) : testsLoading ? ( +

    Loading tests...

    ) : (

    No tests found

    )} diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx index c91549d4a..23e1eae30 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx @@ -21,7 +21,11 @@ type Props = { export function TestProvider({ children, params }: Props) { const router = useRouter(); - const { data } = useTestListQuery({ + const { + data, + loading: testsLoading, + error: testsError, + } = useTestListQuery({ variables: { databaseName: params.databaseName, refName: params.refName, @@ -63,7 +67,12 @@ export function TestProvider({ children, params }: Props) { await saveTestsMutation(); }, [saveTestsMutation, tests]); - // Initialize tests from query data + useEffect(() => { + if (testsError) { + console.error("Error loading tests:", testsError); + } + }, [testsError]); + useEffect(() => { if (data?.tests.list) { const initialTests = data.tests.list.map( @@ -148,97 +157,192 @@ export function TestProvider({ children, params }: Props) { [expandedGroups, setState], ); - const handleRunTest = useCallback( - async (testName: string) => { - const result = await runTests({ - variables: { - databaseName: params.databaseName, - refName: params.refName, - testIdentifier: { - testName, - }, + const handleTestError = useCallback( + (error: string, targetTests: Test[]) => { + const errorResults = targetTests.reduce( + (acc, test) => { + acc[test.testName] = { + status: "failed", + error, + }; + return acc; }, - }); - - const testPassed = - result.data && - result.data.runTests.list.length > 0 && - result.data.runTests.list[0].status === "PASS"; + {} as Record, + ); setState({ testResults: { ...testResults, - [testName]: { - status: testPassed ? "passed" : "failed", - error: testPassed - ? undefined - : result.data?.runTests.list[0].message, - }, + ...errorResults, }, }); }, - [runTests, params.databaseName, params.refName, testResults, setState], + [testResults, setState], + ); + + const handleRunTest = useCallback( + async (testName: string) => { + try { + const result = await runTests({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + testIdentifier: { + testName, + }, + }, + }); + + if (result.error) { + console.error("Error running test:", result.error); + const targetTest = tests.find(t => t.testName === testName); + if (targetTest) { + handleTestError(result.error.message, [targetTest]); + } + return; + } + + const testPassed = + result.data && + result.data.runTests.list.length > 0 && + result.data.runTests.list[0].status === "PASS"; + + setState({ + testResults: { + ...testResults, + [testName]: { + status: testPassed ? "passed" : "failed", + error: testPassed + ? undefined + : result.data?.runTests.list[0].message, + }, + }, + }); + } catch (err) { + console.error("Error running test:", err); + const targetTest = tests.find(t => t.testName === testName); + if (targetTest) { + const errorMessage = + err instanceof Error ? err.message : "Failed to run test"; + handleTestError(errorMessage, [targetTest]); + } + } + }, + [ + runTests, + params.databaseName, + params.refName, + testResults, + setState, + tests, + handleTestError, + ], ); const handleRunGroup = useCallback( async (groupName: string) => { + try { + const result = await runTests({ + variables: { + databaseName: params.databaseName, + refName: params.refName, + testIdentifier: { + groupName, + }, + }, + }); + + if (result.error) { + console.error("Error running group tests:", result.error); + const groupTests = tests.filter(test => test.testGroup === groupName); + handleTestError(result.error.message, groupTests); + return; + } + + result.data?.runTests.list.map(test => + test.status === "PASS" + ? { + status: "passed", + } + : { + status: "failed", + error: test.message, + }, + ); + + const testResultsList = + result.data && result.data.runTests.list.length > 0 + ? result.data.runTests.list + : []; + const groupResults = getResults(testResultsList); + + setState({ + testResults: { + ...testResults, + ...groupResults, + }, + }); + } catch (err) { + console.error("Error running group tests:", err); + const groupTests = tests.filter(test => test.testGroup === groupName); + const errorMessage = + err instanceof Error ? err.message : "Failed to run group tests"; + handleTestError(errorMessage, groupTests); + } + }, + [ + runTests, + params.databaseName, + params.refName, + testResults, + setState, + tests, + handleTestError, + ], + ); + + const handleRunAll = useCallback(async () => { + try { const result = await runTests({ variables: { databaseName: params.databaseName, refName: params.refName, - testIdentifier: { - groupName, - }, }, }); - result.data?.runTests.list.map(test => - test.status === "PASS" - ? { - status: "passed", - } - : { - status: "failed", - error: test.message, - }, - ); + + if (result.error) { + console.error("Error running all tests:", result.error); + handleTestError(result.error.message, tests); + return; + } const testResultsList = result.data && result.data.runTests.list.length > 0 ? result.data.runTests.list : []; - const groupResults = getResults(testResultsList); + const allResults = getResults(testResultsList); setState({ testResults: { ...testResults, - ...groupResults, + ...allResults, }, }); - }, - [runTests, params.databaseName, params.refName, testResults, setState], - ); - - const handleRunAll = useCallback(async () => { - const result = await runTests({ - variables: { - databaseName: params.databaseName, - refName: params.refName, - }, - }); - - const testResultsList = - result.data && result.data.runTests.list.length > 0 - ? result.data.runTests.list - : []; - const allResults = getResults(testResultsList); - - setState({ - testResults: { - ...testResults, - ...allResults, - }, - }); - }, [runTests, params.databaseName, params.refName, testResults, setState]); + } catch (err) { + console.error("Error running all tests:", err); + const errorMessage = + err instanceof Error ? err.message : "Failed to run all tests"; + handleTestError(errorMessage, tests); + } + }, [ + runTests, + params.databaseName, + params.refName, + testResults, + setState, + tests, + handleTestError, + ]); const handleCreateTest = useCallback( (groupName?: string) => { @@ -372,6 +476,8 @@ export function TestProvider({ children, params }: Props) { groupedTests, sortedGroupEntries, testResults, + testsLoading, + testsError: testsError?.message, toggleExpanded, toggleGroupExpanded, handleRunTest, @@ -390,6 +496,8 @@ export function TestProvider({ children, params }: Props) { groupedTests, sortedGroupEntries, testResults, + testsLoading, + testsError, toggleExpanded, toggleGroupExpanded, handleRunTest, diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/state.ts b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/state.ts index ca96beb0a..7aa625576 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/state.ts +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/state.ts @@ -28,6 +28,8 @@ export type TestContextType = { string, { status: "passed" | "failed"; error?: string } | undefined >; + testsLoading: boolean; + testsError?: string; toggleExpanded: (testName: string) => void; toggleGroupExpanded: (groupName: string) => void; handleRunTest: (testName: string) => Promise; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx index e3b4b2c1d..035c73cc1 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/index.tsx @@ -22,9 +22,13 @@ export default function ForTests(props: Props): JSX.Element { routeRefChangeTo={testsUrl} > - {!isPostgres ? - - :
    } + {!isPostgres ? ( + + + + ) : ( +
    + )} ); From 99b2a5f404145a18e1e4d89808b726dbc4285e01 Mon Sep 17 00:00:00 2001 From: eric-richardson1 <102686986+eric-richardson1@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:52:17 -0700 Subject: [PATCH 30/34] Update web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx Co-authored-by: Taylor Bantle --- .../DatabasePage/ForTests/context/index.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx index 23e1eae30..cbfa69378 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx @@ -74,12 +74,11 @@ export function TestProvider({ children, params }: Props) { }, [testsError]); useEffect(() => { - if (data?.tests.list) { - const initialTests = data.tests.list.map( - ({ __typename, ...test }) => test, - ); - setState({ tests: initialTests }); - } + if (!data?.tests.list) return; + const initialTests = data.tests.list.map( + ({ __typename, ...test }) => test, + ); + setState({ tests: initialTests }); }, [data?.tests.list, setState]); useEffect(() => { From ca6d7842a0fcf375845dd27da59a755a5c6b9931 Mon Sep 17 00:00:00 2001 From: eric-richardson1 <102686986+eric-richardson1@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:52:28 -0700 Subject: [PATCH 31/34] Update web/renderer/components/pageComponents/DatabasePage/index.tsx Co-authored-by: Taylor Bantle --- web/renderer/components/pageComponents/DatabasePage/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/index.tsx b/web/renderer/components/pageComponents/DatabasePage/index.tsx index 94c9fb702..b23d00437 100644 --- a/web/renderer/components/pageComponents/DatabasePage/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/index.tsx @@ -14,7 +14,7 @@ import ForRemotes from "./ForRemotes"; import ForSchema from "./ForSchema"; import ForTable from "./ForTable"; import DatabasePage from "./component"; -import ForTests from "@pageComponents/DatabasePage/ForTests"; +import ForTests from "./ForTests"; export default Object.assign(DatabasePage, { ForBranches, From fc81c298870327a606a4a7ffbea6222ff0e553fb Mon Sep 17 00:00:00 2001 From: eric-richardson1 <102686986+eric-richardson1@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:53:11 -0700 Subject: [PATCH 32/34] Update web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx Co-authored-by: Taylor Bantle --- .../pageComponents/DatabasePage/ForTests/TestGroup/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index ed9a18f35..eb22be383 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -143,7 +143,7 @@ export default function TestGroup({ group, className }: Props) { /> {`${testCount} ${pluralize(testCount, "test")}`} + >{testCount} {pluralize(testCount, "test")}
    {groupResult && ( From 2addd3c58396a258736d3fd9a7728e6549a50cba Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Tue, 23 Sep 2025 11:55:45 -0700 Subject: [PATCH 33/34] fix ci --- graphql-server/src/tests/test.resolver.ts | 1 - .../DatabasePage/ForTests/TestGroup/index.tsx | 6 +++--- .../pageComponents/DatabasePage/ForTests/context/index.tsx | 4 +--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/graphql-server/src/tests/test.resolver.ts b/graphql-server/src/tests/test.resolver.ts index a5d5d1aad..7e26520e3 100644 --- a/graphql-server/src/tests/test.resolver.ts +++ b/graphql-server/src/tests/test.resolver.ts @@ -19,7 +19,6 @@ import { @InputType() class TestArgs { - @Field() @Field() testName: string; diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx index eb22be383..4244324bc 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/TestGroup/index.tsx @@ -141,9 +141,9 @@ export default function TestGroup({ group, className }: Props) { onFocus={() => setIsEditing(true)} onClick={e => e.stopPropagation()} /> - {testCount} {pluralize(testCount, "test")} + + {testCount} {pluralize(testCount, "test")} +
    {groupResult && ( diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx index cbfa69378..7932e3913 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx @@ -75,9 +75,7 @@ export function TestProvider({ children, params }: Props) { useEffect(() => { if (!data?.tests.list) return; - const initialTests = data.tests.list.map( - ({ __typename, ...test }) => test, - ); + const initialTests = data.tests.list.map(({ __typename, ...test }) => test); setState({ tests: initialTests }); }, [data?.tests.list, setState]); From 993aba270254e5cb6d6778985a0adbac08b760f3 Mon Sep 17 00:00:00 2001 From: Eric Richardson Date: Tue, 23 Sep 2025 12:16:43 -0700 Subject: [PATCH 34/34] remove dead code --- .../DatabasePage/ForTests/context/index.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx index 7932e3913..0e4cd1933 100644 --- a/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx +++ b/web/renderer/components/pageComponents/DatabasePage/ForTests/context/index.tsx @@ -256,17 +256,6 @@ export function TestProvider({ children, params }: Props) { return; } - result.data?.runTests.list.map(test => - test.status === "PASS" - ? { - status: "passed", - } - : { - status: "failed", - error: test.message, - }, - ); - const testResultsList = result.data && result.data.runTests.list.length > 0 ? result.data.runTests.list