mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 12:33:05 +00:00
add column visibility
This commit is contained in:
@@ -1,11 +1,24 @@
|
|||||||
import { cn, isImageUrl, isUrl } from "@/lib/utils";
|
import { cn, isImageUrl, isUrl } from "@/lib/utils";
|
||||||
import { useTableColumnsQuery, useTableDataQuery } from "@/services/db";
|
import { useTableColumnsQuery, useTableDataQuery } from "@/services/db";
|
||||||
import { useSettingsStore } from "@/state";
|
import { useSettingsStore } from "@/state";
|
||||||
import type { SortingState, VisibilityState } from "@tanstack/react-table";
|
|
||||||
import { Rows3 } from "lucide-react";
|
import { Rows3 } from "lucide-react";
|
||||||
import { Column } from "primereact/column";
|
import { Column } from "primereact/column";
|
||||||
import { DataTable } from "primereact/datatable";
|
import { DataTable, type SortOrder } from "primereact/datatable";
|
||||||
import { useState } from "react";
|
import {
|
||||||
|
MultiSelect,
|
||||||
|
type MultiSelectChangeEvent,
|
||||||
|
} from "primereact/multiselect";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const headerTemplate = ({
|
||||||
|
columnName,
|
||||||
|
udt_name,
|
||||||
|
}: { udt_name: string; columnName: string }) => (
|
||||||
|
<div className={"flex items-center gap-2"}>
|
||||||
|
<span>{columnName}</span>
|
||||||
|
<span className="text-xs text-gray-500">[{udt_name.toUpperCase()}]</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const columnTemplate =
|
const columnTemplate =
|
||||||
({
|
({
|
||||||
@@ -84,17 +97,49 @@ export const DataTablePrime = ({
|
|||||||
const formatDates = useSettingsStore.use.formatDates();
|
const formatDates = useSettingsStore.use.formatDates();
|
||||||
const showImagesPreview = useSettingsStore.use.showImagesPreview();
|
const showImagesPreview = useSettingsStore.use.showImagesPreview();
|
||||||
const paginationOptions = useSettingsStore.use.paginationOptions();
|
const paginationOptions = useSettingsStore.use.paginationOptions();
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
| {
|
||||||
|
sortField?: string;
|
||||||
|
sortOrder?: SortOrder;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(undefined);
|
||||||
const { data: columns } = useTableColumnsQuery({ dbName, tableName });
|
const { data: columns } = useTableColumnsQuery({ dbName, tableName });
|
||||||
|
const [visibleColumns, setVisibleColumns] = useState(columns);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisibleColumns(columns);
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
const onColumnToggle = (event: MultiSelectChangeEvent) => {
|
||||||
|
const selectedColumns = event.value;
|
||||||
|
console.log(selectedColumns);
|
||||||
|
const orderedSelectedColumns = columns?.filter((col) =>
|
||||||
|
selectedColumns.some((sCol: any) => sCol.column_name === col.column_name),
|
||||||
|
);
|
||||||
|
|
||||||
|
setVisibleColumns(orderedSelectedColumns);
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = (
|
||||||
|
<MultiSelect
|
||||||
|
value={visibleColumns}
|
||||||
|
options={columns}
|
||||||
|
optionLabel="column_name"
|
||||||
|
onChange={onColumnToggle}
|
||||||
|
className="w-full sm:w-20rem"
|
||||||
|
display="chip"
|
||||||
|
filter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const { data, isFetching } = useTableDataQuery({
|
const { data, isFetching } = useTableDataQuery({
|
||||||
tableName,
|
tableName,
|
||||||
dbName,
|
dbName,
|
||||||
perPage: pageSize,
|
perPage: pageSize,
|
||||||
page: Math.floor(offset / pageSize),
|
page: Math.floor(offset / pageSize),
|
||||||
sortDesc: sorting[0]?.desc,
|
sortDesc: sorting?.sortOrder === -1,
|
||||||
sortField: sorting[0]?.id,
|
sortField: sorting?.sortField,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -109,6 +154,7 @@ export const DataTablePrime = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 h-full w-full min-w-0">
|
<div className="min-h-0 h-full w-full min-w-0">
|
||||||
<DataTable
|
<DataTable
|
||||||
|
header={header}
|
||||||
rows={pageSize}
|
rows={pageSize}
|
||||||
lazy
|
lazy
|
||||||
first={offset}
|
first={offset}
|
||||||
@@ -119,6 +165,9 @@ export const DataTablePrime = ({
|
|||||||
onPageIndexChange(e.first);
|
onPageIndexChange(e.first);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onSort={setSorting}
|
||||||
|
sortField={sorting?.sortField}
|
||||||
|
sortOrder={sorting?.sortOrder}
|
||||||
paginator
|
paginator
|
||||||
totalRecords={data?.count}
|
totalRecords={data?.count}
|
||||||
rowsPerPageOptions={paginationOptions}
|
rowsPerPageOptions={paginationOptions}
|
||||||
@@ -132,12 +181,19 @@ export const DataTablePrime = ({
|
|||||||
scrollable
|
scrollable
|
||||||
scrollHeight="flex"
|
scrollHeight="flex"
|
||||||
loading={isFetching}
|
loading={isFetching}
|
||||||
|
removableSort
|
||||||
|
stateStorage="local"
|
||||||
|
stateKey={`dt-state-${dbName}-${tableName}`}
|
||||||
>
|
>
|
||||||
{columns?.map((col) => (
|
{visibleColumns?.map((col) => (
|
||||||
<Column
|
<Column
|
||||||
key={col.column_name}
|
key={col.column_name}
|
||||||
|
sortable
|
||||||
field={col.column_name}
|
field={col.column_name}
|
||||||
header={col.column_name}
|
header={headerTemplate({
|
||||||
|
columnName: col.column_name,
|
||||||
|
udt_name: col.udt_name,
|
||||||
|
})}
|
||||||
body={columnTemplate({
|
body={columnTemplate({
|
||||||
columnName: col.column_name,
|
columnName: col.column_name,
|
||||||
formatDates,
|
formatDates,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import "@fontsource/inter/800.css";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { Toaster } from "@/components/ui";
|
import { Toaster } from "@/components/ui";
|
||||||
import { datatable } from "@/styles/datatable";
|
import { datatable } from "@/styles/datatable";
|
||||||
|
import { multiselect } from "@/styles/multiselect";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
@@ -39,6 +40,7 @@ const queryClient = new QueryClient({
|
|||||||
const PrimeStyles = {
|
const PrimeStyles = {
|
||||||
...Tailwind,
|
...Tailwind,
|
||||||
datatable,
|
datatable,
|
||||||
|
multiselect,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render the app
|
// Render the app
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const datatable: DataTablePassThroughOptions = {
|
|||||||
}),
|
}),
|
||||||
header: ({ props }) => ({
|
header: ({ props }) => ({
|
||||||
className: cn(
|
className: cn(
|
||||||
"bg-slate-50 text-slate-700 border-gray-300 font-bold p-4",
|
"font-bold p-4",
|
||||||
"dark:border-blue-900/40 dark:text-white/80 dark:bg-gray-900", // Dark Mode
|
"dark:border-blue-900/40 dark:text-white/80 dark:bg-gray-900", // Dark Mode
|
||||||
props.showGridlines
|
props.showGridlines
|
||||||
? "border-x border-t border-b-0"
|
? "border-x border-t border-b-0"
|
||||||
@@ -48,7 +48,7 @@ export const datatable: DataTablePassThroughOptions = {
|
|||||||
table: "w-full border-spacing-0 text-sm",
|
table: "w-full border-spacing-0 text-sm",
|
||||||
thead: ({ context }) => ({
|
thead: ({ context }) => ({
|
||||||
className: cn({
|
className: cn({
|
||||||
"bg-slate-50 top-0 z-[1]": context.scrollable,
|
"top-0 z-[1]": context.scrollable,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
tbody: ({ props, context }) => ({
|
tbody: ({ props, context }) => ({
|
||||||
@@ -70,7 +70,7 @@ export const datatable: DataTablePassThroughOptions = {
|
|||||||
column: {
|
column: {
|
||||||
headercell: ({ context, props }) => ({
|
headercell: ({ context, props }) => ({
|
||||||
className: cn(
|
className: cn(
|
||||||
"text-left border-0 border-b border-solid border-gray-300 dark:border-blue-900/40 font-bold",
|
"text-left border-0 border-b border-solid font-bold",
|
||||||
"transition duration-200",
|
"transition duration-200",
|
||||||
context?.size === "small"
|
context?.size === "small"
|
||||||
? "p-2"
|
? "p-2"
|
||||||
@@ -79,7 +79,7 @@ export const datatable: DataTablePassThroughOptions = {
|
|||||||
: "p-4", // Size
|
: "p-4", // Size
|
||||||
context.sorted
|
context.sorted
|
||||||
? "bg-blue-50 text-blue-700"
|
? "bg-blue-50 text-blue-700"
|
||||||
: "bg-slate-50 text-slate-700", // Sort
|
: "bg-muted/50 text-slate-700", // Sort
|
||||||
context.sorted
|
context.sorted
|
||||||
? "dark:text-white/80 dark:bg-blue-300"
|
? "dark:text-white/80 dark:bg-blue-300"
|
||||||
: "dark:text-white/80 dark:bg-gray-900", // Dark Mode
|
: "dark:text-white/80 dark:bg-gray-900", // Dark Mode
|
||||||
|
|||||||
165
frontend/src/styles/multiselect.ts
Normal file
165
frontend/src/styles/multiselect.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { MultiSelectPassThroughOptions } from "primereact/multiselect";
|
||||||
|
|
||||||
|
const TRANSITIONS = {
|
||||||
|
overlay: {
|
||||||
|
timeout: 150,
|
||||||
|
classNames: {
|
||||||
|
enter: "opacity-0 scale-75",
|
||||||
|
enterActive:
|
||||||
|
"opacity-100 !scale-100 transition-transform transition-opacity duration-150 ease-in",
|
||||||
|
exit: "opacity-100",
|
||||||
|
exitActive: "!opacity-0 transition-opacity duration-150 ease-linear",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const multiselect: MultiSelectPassThroughOptions = {
|
||||||
|
root: ({ props }) => ({
|
||||||
|
className: cn(
|
||||||
|
"inline-flex cursor-pointer select-none",
|
||||||
|
"bg-white dark:bg-gray-900 border border-gray-400 dark:border-blue-900/40 transition-colors duration-200 ease-in-out rounded-md",
|
||||||
|
"w-full md:w-80",
|
||||||
|
{
|
||||||
|
"opacity-60 select-none pointer-events-none cursor-default":
|
||||||
|
props.disabled,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
labelContainer: "overflow-hidden flex flex-auto cursor-pointer",
|
||||||
|
label: ({ props }) => ({
|
||||||
|
className: cn(
|
||||||
|
"block overflow-hidden whitespace-nowrap cursor-pointer overflow-ellipsis",
|
||||||
|
"text-gray-800 dark:text-white/80",
|
||||||
|
"p-3 transition duration-200",
|
||||||
|
{
|
||||||
|
"!p-3":
|
||||||
|
props.display !== "chip" &&
|
||||||
|
(props.value == null || props.value === undefined),
|
||||||
|
"!py-1.5 px-3": props.display === "chip" && props.value !== null,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
token: {
|
||||||
|
className: cn(
|
||||||
|
"py-1 px-2 mr-2 bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-white/80 rounded-full",
|
||||||
|
"cursor-default inline-flex items-center",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
removeTokenIcon: "ml-2",
|
||||||
|
trigger: {
|
||||||
|
className: cn(
|
||||||
|
"flex items-center justify-center shrink-0",
|
||||||
|
"bg-transparent text-gray-600 dark:text-white/70 w-12 rounded-tr-lg rounded-br-lg",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
className: cn(
|
||||||
|
"bg-white dark:bg-gray-900 text-gray-700 dark:text-white/80 border-0 rounded-md shadow-lg",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
className: cn(
|
||||||
|
"p-3 border-b border-gray-300 dark:border-blue-900/40 text-gray-700 dark:text-white/80 bg-gray-100 dark:bg-gray-800 rounded-t-lg",
|
||||||
|
"flex items-center justify-between",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
headerCheckboxContainer: {
|
||||||
|
className: cn(
|
||||||
|
"inline-flex cursor-pointer select-none align-bottom relative",
|
||||||
|
"mr-2",
|
||||||
|
"w-6 h-6",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
headerCheckbox: {
|
||||||
|
root: ({ props }) => ({
|
||||||
|
className: cn(
|
||||||
|
"asdfasdf flex items-center justify-center [&>div]:shrink-0",
|
||||||
|
"border-2 w-6 h-6 text-gray-600 dark:text-white/70 rounded-lg transition-colors duration-200",
|
||||||
|
"hover:border-blue-500 focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] dark:focus:shadow-[0_0_0_0.2rem_rgba(147,197,253,0.5)]",
|
||||||
|
{
|
||||||
|
"border-gray-300 dark:border-blue-900/40 bg-white dark:bg-gray-900":
|
||||||
|
!props?.checked,
|
||||||
|
"border-blue-500 bg-blue-500": props?.checked,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
headerCheckboxIcon:
|
||||||
|
"w-4 h-4 transition-all duration-200 text-white text-base",
|
||||||
|
closeButton: {
|
||||||
|
className: cn(
|
||||||
|
"flex items-center justify-center overflow-hidden relative",
|
||||||
|
"w-8 h-8 text-gray-500 dark:text-white/70 border-0 bg-transparent rounded-full transition duration-200 ease-in-out mr-2 last:mr-0",
|
||||||
|
"hover:text-gray-700 dark:hover:text-white/80 hover:border-transparent hover:bg-gray-200 dark:hover:bg-gray-800/80 ",
|
||||||
|
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] dark:focus:shadow-[0_0_0_0.2rem_rgba(147,197,253,0.5)]",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
closeIcon: "w-4 h-4 inline-block",
|
||||||
|
wrapper: {
|
||||||
|
className: cn(
|
||||||
|
"max-h-[200px] overflow-auto",
|
||||||
|
"bg-white text-gray-700 border-0 rounded-md shadow-lg",
|
||||||
|
"dark:bg-gray-900 dark:text-white/80",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
list: "py-3 list-none m-0",
|
||||||
|
item: ({ context }) => ({
|
||||||
|
className: cn(
|
||||||
|
"cursor-pointer font-normal overflow-hidden relative whitespace-nowrap",
|
||||||
|
"m-0 p-3 border-0 transition-shadow duration-200 rounded-none",
|
||||||
|
{
|
||||||
|
"text-gray-700 hover:text-gray-700 hover:bg-gray-200 dark:text-white/80 dark:hover:bg-gray-800":
|
||||||
|
!context?.focused && !context?.selected,
|
||||||
|
"bg-gray-300 text-gray-700 dark:text-white/80 dark:bg-gray-800/90 hover:text-gray-700 hover:bg-gray-200 dark:text-white/80 dark:hover:bg-gray-800":
|
||||||
|
context?.focused && !context?.selected,
|
||||||
|
"bg-blue-100 text-blue-700 dark:bg-blue-400 dark:text-white/80":
|
||||||
|
context?.focused && context?.selected,
|
||||||
|
"bg-blue-50 text-blue-700 dark:bg-blue-300 dark:text-white/80":
|
||||||
|
!context?.focused && context?.selected,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
checkboxContainer: {
|
||||||
|
className: cn(
|
||||||
|
"inline-flex cursor-pointer select-none align-bottom relative",
|
||||||
|
"mr-2",
|
||||||
|
"w-6 h-6",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
checkbox: ({ context }) => ({
|
||||||
|
className: cn(
|
||||||
|
"flex items-center justify-center shrink-0",
|
||||||
|
"border-2 w-6 h-6 text-gray-600 dark:text-white/80 rounded-lg transition-colors duration-200",
|
||||||
|
"hover:border-blue-500 focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] dark:focus:shadow-[0_0_0_0.2rem_rgba(147,197,253,0.5)]",
|
||||||
|
{
|
||||||
|
"border-gray-300 dark:border-blue-900/40 bg-white dark:bg-gray-900":
|
||||||
|
!context?.selected,
|
||||||
|
"border-blue-500 bg-blue-500": context?.selected,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
checkboxIcon: "w-4 h-4 transition-all duration-200 text-white text-base",
|
||||||
|
itemGroup: {
|
||||||
|
className: cn(
|
||||||
|
"m-0 p-3 text-gray-800 bg-white font-bold",
|
||||||
|
"dark:bg-gray-900 dark:text-white/80",
|
||||||
|
"cursor-auto",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
filterContainer: "relative",
|
||||||
|
filterInput: {
|
||||||
|
root: {
|
||||||
|
className: cn(
|
||||||
|
"pr-7 -mr-7",
|
||||||
|
"w-full",
|
||||||
|
"font-sans text-base text-gray-700 bg-white py-3 px-3 pr-6 border border-gray-300 transition duration-200 rounded-lg appearance-none",
|
||||||
|
"dark:bg-gray-900 dark:border-blue-900/40 dark:hover:border-blue-300 dark:text-white/80",
|
||||||
|
"hover:border-blue-500 focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] dark:focus:shadow-[0_0_0_0.2rem_rgba(147,197,253,0.5)]",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filterIcon: "-mt-2 absolute top-1/2 right-2",
|
||||||
|
clearIcon: "text-gray-500 right-12 -mt-2 absolute top-1/2",
|
||||||
|
transition: TRANSITIONS.overlay,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user