Skip to content

Commit 0f213e1

Browse files
authored
[BST-263] feature: dashboard page (basetool-io#314)
* create Dashboard model * list all dashboards and add new dashboard * show one dashboard * view and delete dashboard * edit dashboard * small tweaks * refactor sidebar * repair popover * pr comments without security * add a middleware for dashboards and views * repair * remove commented code
1 parent 204ca3a commit 0f213e1

File tree

21 files changed

+1202
-216
lines changed

21 files changed

+1202
-216
lines changed

components/Sidebar.tsx

Lines changed: 11 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,25 @@
1-
import {
2-
ChevronDownIcon,
3-
ChevronLeftIcon,
4-
PencilAltIcon,
5-
PlusCircleIcon,
6-
PlusIcon,
7-
} from "@heroicons/react/outline";
8-
import { Collapse, Tooltip, useDisclosure } from "@chakra-ui/react";
9-
import { ListTable } from "@/plugins/data-sources/abstract-sql-query-service/types";
10-
import { View } from "@prisma/client";
11-
import { first, isUndefined } from "lodash";
1+
import { PencilAltIcon } from "@heroicons/react/outline";
122
import { useACLHelpers } from "@/features/authorization/hooks";
13-
import { useDataSourceContext, useProfile } from "@/hooks";
3+
import { useDataSourceContext } from "@/hooks";
144
import { useDataSourceResponse } from "@/features/data-sources/hooks";
15-
import { useGetTablesQuery } from "@/features/tables/api-slice";
16-
import { useGetViewsQuery } from "@/features/views/api-slice";
17-
import { usePrefetch } from "@/features/fields/api-slice";
5+
import DashboardSidebarSection from "@/features/dashboards/components/DashboardsSidebarSection";
186
import Link from "next/link";
19-
import React, { memo, useMemo } from "react";
7+
import React, { memo } from "react";
208
import Shimmer from "./Shimmer";
21-
import SidebarItem from "./SidebarItem";
9+
import TablesSidebarSection from "@/features/tables/components/TablesSidebarSection";
10+
import ViewsSidebarSection from "@/features/views/components/ViewsSidebarSection";
2211

2312
const Sidebar = () => {
24-
const { dataSourceId, tableName, viewId } = useDataSourceContext();
25-
26-
const { user, isLoading: sessionIsLoading } = useProfile();
27-
13+
const { dataSourceId } = useDataSourceContext();
2814
const {
2915
dataSource,
30-
info,
3116
isLoading: dataSourceIsLoading,
3217
info: dataSourceInfo,
3318
} = useDataSourceResponse(dataSourceId);
34-
3519
const { isOwner } = useACLHelpers({ dataSourceInfo });
3620

37-
const {
38-
data: tablesResponse,
39-
isLoading: tablesAreLoading,
40-
error: tablesError,
41-
} = useGetTablesQuery({ dataSourceId }, { skip: !dataSourceId });
42-
43-
const {
44-
data: viewsResponse,
45-
isLoading: viewsAreLoading,
46-
error: viewsError,
47-
} = useGetViewsQuery();
48-
49-
const prefetchColumns = usePrefetch("getColumns");
50-
51-
const { isOpen: isTablesOpen, onToggle: toggleTablesOpen } = useDisclosure({
52-
defaultIsOpen: true,
53-
});
54-
const { isOpen: isViewsOpen, onToggle: toggleViewsOpen } = useDisclosure({
55-
defaultIsOpen: true,
56-
});
57-
58-
const views = useMemo(
59-
() => (viewsResponse?.ok ? viewsResponse?.data : []),
60-
[viewsResponse]
61-
);
62-
63-
const filteredViews = useMemo(
64-
() =>
65-
views.filter(
66-
(view: View) =>
67-
(view.createdBy === user.id || view.public === true) &&
68-
view.dataSourceId === parseInt(dataSourceId)
69-
),
70-
[views, dataSourceId]
71-
);
72-
73-
const viewsLoading = useMemo(
74-
() => viewsAreLoading || sessionIsLoading,
75-
[viewsAreLoading || sessionIsLoading]
76-
);
77-
78-
const tablesLoading = useMemo(
79-
() => tablesAreLoading || sessionIsLoading,
80-
[tablesAreLoading || sessionIsLoading]
81-
);
82-
83-
const ViewsSection = () => (
84-
<>
85-
<div className="relative space-y-1 flex-col">
86-
<div className="flex justify-between w-full">
87-
<div
88-
className="text-md font-semibold py-2 px-2 rounded-md leading-none m-0 w-full cursor-pointer"
89-
onClick={toggleViewsOpen}
90-
>
91-
Views{" "}
92-
{isViewsOpen ? (
93-
<ChevronDownIcon className="h-3 inline" />
94-
) : (
95-
<ChevronLeftIcon className="h-3 inline" />
96-
)}
97-
</div>
98-
{viewsResponse?.ok &&
99-
viewsResponse.data.filter(
100-
(view: View) =>
101-
(view.createdBy === user.id || view.public === true) &&
102-
view.dataSourceId === parseInt(dataSourceId)
103-
).length > 0 && (
104-
<Link href={`/views/new?dataSourceId=${dataSourceId}`}>
105-
<a className="flex justify-center items-center mx-2">
106-
<Tooltip label="Add view">
107-
<div>
108-
<PlusCircleIcon className="h-4 inline cursor-pointer" />
109-
</div>
110-
</Tooltip>
111-
</a>
112-
</Link>
113-
)}
114-
</div>
115-
116-
<Collapse in={isViewsOpen}>
117-
{viewsLoading && (
118-
<div className="flex-1 min-h-full px-1 space-y-2 mt-3">
119-
<Shimmer height={16} width={50} />
120-
<Shimmer height={16} width={90} />
121-
<Shimmer height={16} width={110} />
122-
<Shimmer height={16} width={60} />
123-
</div>
124-
)}
125-
{/* If no views are present, show a nice box with the create message */}
126-
{!viewsLoading && filteredViews.length === 0 && (
127-
<Link href={`/views/new?dataSourceId=${dataSourceId}`} passHref>
128-
<div className="flex justify-center items-center border-2 rounded-md border-dashed border-gray-500 py-6 text-gray-600 cursor-pointer mb-2">
129-
<PlusIcon className="h-4 mr-1 flex flex-shrink-0" />
130-
Create view
131-
</div>
132-
</Link>
133-
)}
134-
{/* display only views created by logged in user or public views and having same datasource */}
135-
{!viewsLoading &&
136-
filteredViews.map((view: View, idx: number) => (
137-
<SidebarItem
138-
key={idx}
139-
active={view.id === parseInt(viewId)}
140-
label={view.name}
141-
link={`/views/${view.id}`}
142-
/>
143-
))}
144-
</Collapse>
145-
</div>
146-
<hr className="mt-2 mb-2" />
147-
</>
148-
);
149-
15021
return (
151-
<div className="relative py-2 pl-2 w-full overflow-y-auto">
22+
<div className="relative py-2 pl-2 w-full">
15223
<div className="relative space-y-x w-full h-full flex flex-col">
15324
<div className="my-2 mt-4 px-2 font-bold uppercase text leading-none">
15425
{dataSourceIsLoading && (
@@ -170,82 +41,9 @@ const Sidebar = () => {
17041
)}
17142
</div>
17243
<hr className="-mt-px mb-2" />
173-
{viewsError && (
174-
<div>
175-
{"data" in viewsError && first((viewsError?.data as any)?.messages)}
176-
</div>
177-
)}
178-
{dataSourceInfo?.supports?.views && <ViewsSection />}
179-
{isOwner && (
180-
<>
181-
{tablesError && (
182-
<div>
183-
{"data" in tablesError &&
184-
first((tablesError?.data as any)?.messages)}
185-
</div>
186-
)}
187-
<div className="relative space-y-1 flex-1">
188-
<div
189-
className="text-md font-semibold py-2 px-2 rounded-md leading-none m-0 cursor-pointer"
190-
onClick={toggleTablesOpen}
191-
>
192-
Tables{" "}
193-
<span className="text-xs text-gray-500">
194-
(visible only to owners)
195-
</span>
196-
{isTablesOpen ? (
197-
<ChevronDownIcon className="h-3 inline" />
198-
) : (
199-
<ChevronLeftIcon className="h-3 inline" />
200-
)}
201-
</div>
202-
<Collapse in={isTablesOpen}>
203-
<div className="">
204-
{tablesLoading && (
205-
<div className="flex-1 min-h-full px-1 space-y-2 mt-3">
206-
<Shimmer height={16} width={50} />
207-
<Shimmer height={16} width={60} />
208-
<Shimmer height={16} width={120} />
209-
<Shimmer height={16} width={90} />
210-
<Shimmer height={16} width={60} />
211-
<Shimmer height={16} width={110} />
212-
<Shimmer height={16} width={90} />
213-
</div>
214-
)}
215-
{/* @todo: why does the .data attribute remain populated with old content when the hooks has changed? */}
216-
{/* Got to a valid DS and then to an invalid one. the data attribute will still have the old data there. */}
217-
{!tablesLoading &&
218-
tablesResponse?.ok &&
219-
tablesResponse.data
220-
.filter((table: ListTable) =>
221-
dataSource?.type === "postgresql" && table.schema
222-
? table.schema === "public"
223-
: true
224-
)
225-
.map((table: ListTable, idx: number) => (
226-
<SidebarItem
227-
key={idx}
228-
active={
229-
table.name === tableName && isUndefined(viewId)
230-
}
231-
label={table.name}
232-
link={`/data-sources/${dataSourceId}/tables/${table.name}`}
233-
onMouseOver={() => {
234-
// If the datasource supports columns request we'll prefetch it on hover.
235-
if (info?.supports?.columnsRequest) {
236-
prefetchColumns({
237-
dataSourceId,
238-
tableName: table.name,
239-
});
240-
}
241-
}}
242-
/>
243-
))}
244-
</div>
245-
</Collapse>
246-
</div>
247-
</>
248-
)}
44+
<DashboardSidebarSection />
45+
{dataSourceInfo?.supports?.views && <ViewsSidebarSection />}
46+
{isOwner && <TablesSidebarSection />}
24947
</div>
25048
</div>
25149
);

features/dashboards/api-slice.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { apiUrl } from "@/features/api/urls";
2+
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
3+
import ApiResponse from "@/features/api/ApiResponse";
4+
import URI from "urijs";
5+
6+
export const api = createApi({
7+
reducerPath: "dashboards",
8+
baseQuery: fetchBaseQuery({
9+
baseUrl: `${apiUrl}`,
10+
}),
11+
tagTypes: ["Dashboard"],
12+
endpoints(builder) {
13+
return {
14+
addDashboard: builder.mutation<
15+
ApiResponse,
16+
Partial<{
17+
body: unknown;
18+
}>
19+
>({
20+
query: ({ body }) => ({
21+
url: `${apiUrl}/dashboards`,
22+
method: "POST",
23+
body,
24+
}),
25+
invalidatesTags: [{ type: "Dashboard", id: "LIST" }],
26+
}),
27+
getDashboards: builder.query<ApiResponse, { dataSourceId?: string; }>({
28+
query({dataSourceId}) {
29+
const queryParams = URI()
30+
.query({
31+
dataSourceId,
32+
})
33+
.query()
34+
.toString();
35+
36+
return `/dashboards?${queryParams}`;
37+
},
38+
providesTags: [{ type: "Dashboard", id: "LIST" }],
39+
}),
40+
getDashboard: builder.query<ApiResponse, Partial<{ dashboardId: string }>>({
41+
query({ dashboardId }) {
42+
return `/dashboards/${dashboardId}`;
43+
},
44+
providesTags: (result, error, { dashboardId }) => [
45+
{ type: "Dashboard", id: dashboardId },
46+
],
47+
}),
48+
removeDashboard: builder.mutation<ApiResponse, Partial<{ dashboardId: string }>>({
49+
query: ({ dashboardId }) => ({
50+
url: `${apiUrl}/dashboards/${dashboardId}`,
51+
method: "DELETE",
52+
}),
53+
invalidatesTags: (result, error, { dashboardId }) => [
54+
{ type: "Dashboard", id: "LIST" },
55+
{ type: "Dashboard", id: dashboardId },
56+
],
57+
}),
58+
updateDashboard: builder.mutation<
59+
ApiResponse,
60+
Partial<{
61+
dashboardId: string;
62+
body: unknown;
63+
}>
64+
>({
65+
query: ({ dashboardId, body }) => ({
66+
url: `${apiUrl}/dashboards/${dashboardId}`,
67+
method: "PUT",
68+
body,
69+
}),
70+
invalidatesTags: (result, error, { dashboardId }) => [
71+
{ type: "Dashboard", id: "LIST" },
72+
{ type: "Dashboard", id: dashboardId },
73+
],
74+
}),
75+
};
76+
},
77+
});
78+
79+
export const {
80+
useAddDashboardMutation,
81+
useGetDashboardsQuery,
82+
useGetDashboardQuery,
83+
useRemoveDashboardMutation,
84+
useUpdateDashboardMutation
85+
} = api;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useDataSourceContext } from "@/hooks";
2+
import { useDataSourceResponse } from "@/features/data-sources/hooks"
3+
import React from "react";
4+
import Shimmer from "@/components/Shimmer";
5+
import TinyLabel from "@/components/TinyLabel";
6+
7+
function DashboardEditDataSourceInfo() {
8+
const { dataSourceId } = useDataSourceContext();
9+
const { dataSource, isLoading: dataSourceIsLoading } =
10+
useDataSourceResponse(dataSourceId);
11+
12+
return (
13+
<div className="grid space-y-4 lg:space-y-0 lg:grid-cols-2">
14+
<div>
15+
<TinyLabel className="mr-1">DataSource</TinyLabel>
16+
<div className="text-sm flex-1">
17+
{dataSourceIsLoading && (
18+
<Shimmer height="14px" width="70px" className="mt-1" />
19+
)}
20+
{!dataSourceIsLoading && dataSource?.name}
21+
</div>
22+
</div>
23+
</div>
24+
);
25+
}
26+
27+
export default DashboardEditDataSourceInfo;

0 commit comments

Comments
 (0)