@izumisy-tailor/tailor-data-viewer 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { useState, useCallback } from "react";
|
|
2
|
-
import { Plus, X
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { Plus, X } from "lucide-react";
|
|
3
3
|
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
|
4
4
|
import { Button } from "./ui/button";
|
|
5
|
-
import type {
|
|
6
|
-
|
|
5
|
+
import type {
|
|
6
|
+
TableMetadata,
|
|
7
|
+
TableMetadataMap,
|
|
8
|
+
} from "../generator/metadata-generator";
|
|
7
9
|
import { DataViewTabContent } from "./data-view-tab-content";
|
|
8
10
|
import { SingleRecordTabContent } from "./single-record-tab-content";
|
|
9
11
|
import { useSavedViews, type SavedView } from "./saved-view-context";
|
|
@@ -54,33 +56,14 @@ export function DataViewer({
|
|
|
54
56
|
appUri,
|
|
55
57
|
initialViewId,
|
|
56
58
|
}: DataViewerProps) {
|
|
57
|
-
// Get tables accessible to the current user via runtime permission check
|
|
58
59
|
const allTables = Object.values(tableMetadata);
|
|
59
|
-
const {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
const { getViewById } = useSavedViews();
|
|
60
|
+
const { getViewById, isLoading } = useSavedViews();
|
|
61
|
+
|
|
62
|
+
// Track if we've already initialized from the saved view
|
|
63
|
+
const initializedFromViewRef = useRef(false);
|
|
64
64
|
|
|
65
|
-
// Tab management -
|
|
65
|
+
// Tab management - start with empty tab, will apply saved view after loading
|
|
66
66
|
const [tabs, setTabs] = useState<Tab[]>(() => {
|
|
67
|
-
if (initialViewId) {
|
|
68
|
-
const view = getViewById(initialViewId);
|
|
69
|
-
if (view) {
|
|
70
|
-
const table = tableMetadata[view.tableName];
|
|
71
|
-
if (table) {
|
|
72
|
-
return [
|
|
73
|
-
{
|
|
74
|
-
id: generateTabId(),
|
|
75
|
-
label: view.name,
|
|
76
|
-
table,
|
|
77
|
-
isLocked: true,
|
|
78
|
-
initialView: view,
|
|
79
|
-
},
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
67
|
return [
|
|
85
68
|
{
|
|
86
69
|
id: generateTabId(),
|
|
@@ -92,6 +75,31 @@ export function DataViewer({
|
|
|
92
75
|
});
|
|
93
76
|
const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id);
|
|
94
77
|
|
|
78
|
+
// Apply saved view after views are loaded from store
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
// Skip if no initialViewId, already initialized, or still loading
|
|
81
|
+
if (!initialViewId || initializedFromViewRef.current || isLoading) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const view = getViewById(initialViewId);
|
|
86
|
+
if (view) {
|
|
87
|
+
const table = tableMetadata[view.tableName];
|
|
88
|
+
if (table) {
|
|
89
|
+
initializedFromViewRef.current = true;
|
|
90
|
+
const newTab: Tab = {
|
|
91
|
+
id: generateTabId(),
|
|
92
|
+
label: view.name,
|
|
93
|
+
table,
|
|
94
|
+
isLocked: true,
|
|
95
|
+
initialView: view,
|
|
96
|
+
};
|
|
97
|
+
setTabs([newTab]);
|
|
98
|
+
setActiveTabId(newTab.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [initialViewId, isLoading, getViewById, tableMetadata]);
|
|
102
|
+
|
|
95
103
|
const handleAddTab = useCallback(() => {
|
|
96
104
|
const newTab: Tab = {
|
|
97
105
|
id: generateTabId(),
|
|
@@ -188,45 +196,6 @@ export function DataViewer({
|
|
|
188
196
|
[tableMetadata],
|
|
189
197
|
);
|
|
190
198
|
|
|
191
|
-
// Loading state during permission check
|
|
192
|
-
if (isLoading) {
|
|
193
|
-
return (
|
|
194
|
-
<Card>
|
|
195
|
-
<CardContent className="py-8">
|
|
196
|
-
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
|
197
|
-
<Loader2 className="size-6 animate-spin" />
|
|
198
|
-
<span>テーブルへのアクセス権限を確認中...</span>
|
|
199
|
-
</div>
|
|
200
|
-
</CardContent>
|
|
201
|
-
</Card>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Error state
|
|
206
|
-
if (error) {
|
|
207
|
-
return (
|
|
208
|
-
<Card>
|
|
209
|
-
<CardContent className="py-8">
|
|
210
|
-
<div className="text-destructive text-center">
|
|
211
|
-
アクセス権限の確認中にエラーが発生しました: {error}
|
|
212
|
-
</div>
|
|
213
|
-
</CardContent>
|
|
214
|
-
</Card>
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (accessibleTables.length === 0) {
|
|
219
|
-
return (
|
|
220
|
-
<Card>
|
|
221
|
-
<CardContent className="py-8">
|
|
222
|
-
<div className="text-muted-foreground text-center">
|
|
223
|
-
アクセス可能なテーブルがありません
|
|
224
|
-
</div>
|
|
225
|
-
</CardContent>
|
|
226
|
-
</Card>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
199
|
return (
|
|
231
200
|
<Card>
|
|
232
201
|
<CardHeader className="pb-2">
|
|
@@ -289,7 +258,7 @@ export function DataViewer({
|
|
|
289
258
|
/>
|
|
290
259
|
) : (
|
|
291
260
|
<DataViewTabContent
|
|
292
|
-
tables={
|
|
261
|
+
tables={allTables}
|
|
293
262
|
appUri={appUri}
|
|
294
263
|
initialTable={tab.table ?? undefined}
|
|
295
264
|
onTableConfirm={(table) => handleTableConfirm(tab.id, table)}
|
package/src/component/index.ts
CHANGED
|
@@ -9,10 +9,6 @@ export { SearchFilterForm } from "./search-filter";
|
|
|
9
9
|
export { ViewSave } from "./view-save-load";
|
|
10
10
|
export { useTableData } from "./hooks/use-table-data";
|
|
11
11
|
export { useColumnState } from "./hooks/use-column-state";
|
|
12
|
-
export {
|
|
13
|
-
useTableAccessCheck,
|
|
14
|
-
checkTableAccess,
|
|
15
|
-
} from "./hooks/use-table-access-check";
|
|
16
12
|
export { useSavedViews, SavedViewProvider } from "./saved-view-context";
|
|
17
13
|
export type { SavedView, SaveViewInput } from "./saved-view-context";
|
|
18
14
|
export type { SearchFilter, SearchFilters } from "./types";
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
TableMetadata,
|
|
4
|
-
TableMetadataMap,
|
|
5
|
-
} from "../../generator/metadata-generator";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Filter tables based on user's roles
|
|
9
|
-
* Returns only tables the user has read access to
|
|
10
|
-
*/
|
|
11
|
-
export function useAccessibleTables(
|
|
12
|
-
tableMetadata: TableMetadataMap,
|
|
13
|
-
userRoles: string[],
|
|
14
|
-
): TableMetadata[] {
|
|
15
|
-
return useMemo(() => {
|
|
16
|
-
return Object.values(tableMetadata).filter((table) =>
|
|
17
|
-
table.readAllowedRoles.some((role) =>
|
|
18
|
-
userRoles.map((r) => r.toUpperCase()).includes(role.toUpperCase()),
|
|
19
|
-
),
|
|
20
|
-
);
|
|
21
|
-
}, [tableMetadata, userRoles]);
|
|
22
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo } from "react";
|
|
2
|
-
import { GraphQLClient } from "graphql-request";
|
|
3
|
-
import type { TableMetadata } from "../../generator/metadata-generator";
|
|
4
|
-
|
|
5
|
-
export interface TableAccessCheckResult {
|
|
6
|
-
/** Tables that the user has access to */
|
|
7
|
-
accessibleTables: TableMetadata[];
|
|
8
|
-
/** Whether the check is in progress */
|
|
9
|
-
isLoading: boolean;
|
|
10
|
-
/** Error message if the check failed entirely */
|
|
11
|
-
error: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check which tables the user has access to by executing aggregate queries
|
|
16
|
-
* Uses server-side gqlPermission as the source of truth
|
|
17
|
-
*/
|
|
18
|
-
export function useTableAccessCheck(
|
|
19
|
-
tables: TableMetadata[],
|
|
20
|
-
appUri: string,
|
|
21
|
-
): TableAccessCheckResult {
|
|
22
|
-
const [accessibleTables, setAccessibleTables] = useState<TableMetadata[]>([]);
|
|
23
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
24
|
-
const [error, setError] = useState<string | null>(null);
|
|
25
|
-
|
|
26
|
-
// Memoize tables to prevent infinite loops
|
|
27
|
-
// when the parent component passes a new array reference on each render
|
|
28
|
-
const tableKey = tables.map((t) => t.name).join(",");
|
|
29
|
-
const stableTables = useMemo(() => tables, [tableKey]);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
let cancelled = false;
|
|
33
|
-
|
|
34
|
-
async function checkAccess() {
|
|
35
|
-
setIsLoading(true);
|
|
36
|
-
setError(null);
|
|
37
|
-
|
|
38
|
-
const client = new GraphQLClient(`${appUri}/query`, {
|
|
39
|
-
credentials: "include",
|
|
40
|
-
headers: {
|
|
41
|
-
"Content-Type": "application/json",
|
|
42
|
-
"X-Tailor-Nonce": crypto.randomUUID(),
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const results = await Promise.allSettled(
|
|
47
|
-
stableTables.map(async (table) => {
|
|
48
|
-
// Execute a lightweight aggregate query to check access
|
|
49
|
-
const query = `query CheckAccess { ${table.pluralForm}(query: {}) { total } }`;
|
|
50
|
-
await client.request(query);
|
|
51
|
-
return table;
|
|
52
|
-
}),
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (cancelled) return;
|
|
56
|
-
|
|
57
|
-
// Filter to only fulfilled results (tables with access)
|
|
58
|
-
const accessible = results
|
|
59
|
-
.filter(
|
|
60
|
-
(r): r is PromiseFulfilledResult<TableMetadata> =>
|
|
61
|
-
r.status === "fulfilled",
|
|
62
|
-
)
|
|
63
|
-
.map((r) => r.value);
|
|
64
|
-
|
|
65
|
-
setAccessibleTables(accessible);
|
|
66
|
-
setIsLoading(false);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
checkAccess().catch((err) => {
|
|
70
|
-
if (!cancelled) {
|
|
71
|
-
setError(
|
|
72
|
-
err instanceof Error ? err.message : "Failed to check table access",
|
|
73
|
-
);
|
|
74
|
-
setIsLoading(false);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return () => {
|
|
79
|
-
cancelled = true;
|
|
80
|
-
};
|
|
81
|
-
}, [stableTables, appUri]);
|
|
82
|
-
|
|
83
|
-
return { accessibleTables, isLoading, error };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Standalone function to check table access (non-hook version)
|
|
88
|
-
* Useful for one-time checks or server-side usage
|
|
89
|
-
*/
|
|
90
|
-
export async function checkTableAccess(
|
|
91
|
-
client: GraphQLClient,
|
|
92
|
-
tables: TableMetadata[],
|
|
93
|
-
): Promise<TableMetadata[]> {
|
|
94
|
-
const results = await Promise.allSettled(
|
|
95
|
-
tables.map(async (table) => {
|
|
96
|
-
const query = `query CheckAccess { ${table.pluralForm}(query: {}) { total } }`;
|
|
97
|
-
await client.request(query);
|
|
98
|
-
return table;
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
return results
|
|
103
|
-
.filter(
|
|
104
|
-
(r): r is PromiseFulfilledResult<TableMetadata> =>
|
|
105
|
-
r.status === "fulfilled",
|
|
106
|
-
)
|
|
107
|
-
.map((r) => r.value);
|
|
108
|
-
}
|