Spaces:
Running
Running
Mark Duppenthaler
commited on
Commit
·
54be5f9
1
Parent(s):
cf4e7fe
Add dataset selector
Browse files- backend/app.py +13 -40
- backend/config.py +109 -0
- frontend/src/App.tsx +5 -20
- frontend/src/components/DataChart.tsx +3 -3
- frontend/src/components/DatasetSelector.tsx +37 -0
- frontend/src/components/LeaderBoardPage.tsx +34 -0
- frontend/src/components/LeaderboardTable.tsx +4 -5
backend/app.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
from backend.chart import mk_variations
|
|
|
2 |
from backend.examples import audio_examples_tab, image_examples_tab, video_examples_tab
|
3 |
from flask import Flask, Response, send_from_directory
|
4 |
from flask_cors import CORS
|
@@ -35,19 +36,20 @@ def index():
|
|
35 |
return send_from_directory(app.static_folder, "index.html")
|
36 |
|
37 |
|
38 |
-
@app.route("/data/<path:
|
39 |
-
def data_files(
|
40 |
"""
|
41 |
Serves csv files from the data directory.
|
42 |
"""
|
43 |
data_dir = os.path.join(os.path.dirname(__file__), "data")
|
44 |
-
file_path = os.path.join(data_dir,
|
|
|
45 |
if os.path.isfile(file_path):
|
46 |
df = pd.read_csv(file_path)
|
47 |
-
logger.info(f"Processing
|
48 |
-
if
|
49 |
-
return get_leaderboard(df)
|
50 |
-
elif
|
51 |
return get_chart(df)
|
52 |
|
53 |
return "File not found", 404
|
@@ -118,43 +120,14 @@ def proxy(url):
|
|
118 |
return {"error": str(e)}, 500
|
119 |
|
120 |
|
121 |
-
def get_leaderboard(df):
|
122 |
# Determine file type and handle accordingly
|
123 |
-
|
124 |
-
# Modify the dataframe - you'll need to define first_cols and attack_scores
|
125 |
-
first_cols = [
|
126 |
-
"snr",
|
127 |
-
"sisnr",
|
128 |
-
"stoi",
|
129 |
-
"pesq",
|
130 |
-
] # Define appropriate values based on your needs
|
131 |
-
attack_scores = [
|
132 |
-
"bit_acc",
|
133 |
-
"log10_p_value",
|
134 |
-
"TPR",
|
135 |
-
"FPR",
|
136 |
-
] # Define appropriate values based on your needs
|
137 |
-
categories = {
|
138 |
-
"speed": "Time",
|
139 |
-
"updownresample": "Time",
|
140 |
-
"echo": "Time",
|
141 |
-
"random_noise": "Amplitude",
|
142 |
-
"lowpass_filter": "Amplitude",
|
143 |
-
"highpass_filter": "Amplitude",
|
144 |
-
"bandpass_filter": "Amplitude",
|
145 |
-
"smooth": "Amplitude",
|
146 |
-
"boost_audio": "Amplitude",
|
147 |
-
"duck_audio": "Amplitude",
|
148 |
-
"shush": "Amplitude",
|
149 |
-
"pink_noise": "Amplitude",
|
150 |
-
"aac_compression": "Compression",
|
151 |
-
"mp3_compression": "Compression",
|
152 |
-
}
|
153 |
|
154 |
# This part adds on all the columns
|
155 |
-
df = get_old_format_dataframe(df, first_cols, attack_scores)
|
156 |
|
157 |
-
groups, default_selection = get_leaderboard_filters(df, categories)
|
158 |
|
159 |
# Replace NaN values with None for JSON serialization
|
160 |
df = df.fillna(value="NaN")
|
|
|
1 |
from backend.chart import mk_variations
|
2 |
+
from backend.config import get_dataset_config
|
3 |
from backend.examples import audio_examples_tab, image_examples_tab, video_examples_tab
|
4 |
from flask import Flask, Response, send_from_directory
|
5 |
from flask_cors import CORS
|
|
|
36 |
return send_from_directory(app.static_folder, "index.html")
|
37 |
|
38 |
|
39 |
+
@app.route("/data/<path:dataset_name>")
|
40 |
+
def data_files(dataset_name):
|
41 |
"""
|
42 |
Serves csv files from the data directory.
|
43 |
"""
|
44 |
data_dir = os.path.join(os.path.dirname(__file__), "data")
|
45 |
+
file_path = os.path.join(data_dir, dataset_name) + ".csv"
|
46 |
+
logger.info(f"Looking for dataset file: {file_path}")
|
47 |
if os.path.isfile(file_path):
|
48 |
df = pd.read_csv(file_path)
|
49 |
+
logger.info(f"Processing dataset: {dataset_name}")
|
50 |
+
if dataset_name.endswith("benchmark"):
|
51 |
+
return get_leaderboard(dataset_name, df)
|
52 |
+
elif dataset_name.endswith("attacks_variations"):
|
53 |
return get_chart(df)
|
54 |
|
55 |
return "File not found", 404
|
|
|
120 |
return {"error": str(e)}, 500
|
121 |
|
122 |
|
123 |
+
def get_leaderboard(dataset_name, df):
|
124 |
# Determine file type and handle accordingly
|
125 |
+
config = get_dataset_config(dataset_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
|
127 |
# This part adds on all the columns
|
128 |
+
df = get_old_format_dataframe(df, config["first_cols"], config["attack_scores"])
|
129 |
|
130 |
+
groups, default_selection = get_leaderboard_filters(df, config["categories"])
|
131 |
|
132 |
# Replace NaN values with None for JSON serialization
|
133 |
df = df.fillna(value="NaN")
|
backend/config.py
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def get_dataset_config(dataset_name):
|
2 |
+
if dataset_name == "voxpopuli_1k_audio_benchmark":
|
3 |
+
return {
|
4 |
+
"first_cols": [
|
5 |
+
"snr",
|
6 |
+
"sisnr",
|
7 |
+
"stoi",
|
8 |
+
"pesq",
|
9 |
+
],
|
10 |
+
"attack_scores": [
|
11 |
+
"bit_acc",
|
12 |
+
"log10_p_value",
|
13 |
+
"TPR",
|
14 |
+
"FPR",
|
15 |
+
],
|
16 |
+
"categories": {
|
17 |
+
"speed": "Time",
|
18 |
+
"updownresample": "Time",
|
19 |
+
"echo": "Time",
|
20 |
+
"random_noise": "Amplitude",
|
21 |
+
"lowpass_filter": "Amplitude",
|
22 |
+
"highpass_filter": "Amplitude",
|
23 |
+
"bandpass_filter": "Amplitude",
|
24 |
+
"smooth": "Amplitude",
|
25 |
+
"boost_audio": "Amplitude",
|
26 |
+
"duck_audio": "Amplitude",
|
27 |
+
"shush": "Amplitude",
|
28 |
+
"pink_noise": "Amplitude",
|
29 |
+
"aac_compression": "Compression",
|
30 |
+
"mp3_compression": "Compression",
|
31 |
+
},
|
32 |
+
}
|
33 |
+
elif dataset_name == "ravdess_1k_audio_benchmark":
|
34 |
+
return {
|
35 |
+
"first_cols": ["snr", "sisnr", "stoi", "pesq"],
|
36 |
+
"attack_scores": ["bit_acc", "log10_p_value", "TPR", "FPR"],
|
37 |
+
"categories": {
|
38 |
+
"speed": "Time",
|
39 |
+
"updownresample": "Time",
|
40 |
+
"echo": "Time",
|
41 |
+
"random_noise": "Amplitude",
|
42 |
+
"lowpass_filter": "Amplitude",
|
43 |
+
"highpass_filter": "Amplitude",
|
44 |
+
"bandpass_filter": "Amplitude",
|
45 |
+
"smooth": "Amplitude",
|
46 |
+
"boost_audio": "Amplitude",
|
47 |
+
"duck_audio": "Amplitude",
|
48 |
+
"shush": "Amplitude",
|
49 |
+
"pink_noise": "Amplitude",
|
50 |
+
"aac_compression": "Compression",
|
51 |
+
"mp3_compression": "Compression",
|
52 |
+
},
|
53 |
+
}
|
54 |
+
elif dataset_name == "val2014_1k_image_benchmark":
|
55 |
+
return {
|
56 |
+
"first_cols": ["psnr", "ssim", "lpips", "decoder_time"],
|
57 |
+
"attack_scores": ["bit_acc", "log10_p_value", "TPR", "FPR"],
|
58 |
+
"categories": {
|
59 |
+
"proportion": "Geometric",
|
60 |
+
"collage": "Inpainting",
|
61 |
+
"center_crop": "Geometric",
|
62 |
+
"rotate": "Geometric",
|
63 |
+
"jpeg": "Compression",
|
64 |
+
"brightness": "Visual",
|
65 |
+
"contrast": "Visual",
|
66 |
+
"saturation": "Visual",
|
67 |
+
"sharpness": "Visual",
|
68 |
+
"resize": "Geometric",
|
69 |
+
"overlay_text": "Inpainting",
|
70 |
+
"hflip": "Geometric",
|
71 |
+
"perspective": "Geometric",
|
72 |
+
"median_filter": "Visual",
|
73 |
+
"hue": "Visual",
|
74 |
+
"gaussian_blur": "Visual",
|
75 |
+
"comb": "Mixed",
|
76 |
+
"avg": "Averages",
|
77 |
+
"none": "Baseline",
|
78 |
+
},
|
79 |
+
}
|
80 |
+
elif dataset_name == "sav_val_full_video_benchmark":
|
81 |
+
return {
|
82 |
+
"first_cols": ["psnr", "ssim", "msssim", "lpips", "vmaf", "decoder_time"],
|
83 |
+
"attack_scores": ["bit_acc", "log10_p_value", "TPR", "FPR"],
|
84 |
+
"categories": {
|
85 |
+
"HorizontalFlip": "Geometric",
|
86 |
+
"Rotate": "Geometric",
|
87 |
+
"Resize": "Geometric",
|
88 |
+
"Crop": "Geometric",
|
89 |
+
"Perspective": "Geometric",
|
90 |
+
"Brightness": "Visual",
|
91 |
+
"Contrast": "Visual",
|
92 |
+
"Saturation": "Visual",
|
93 |
+
"Grayscale": "Visual",
|
94 |
+
"Hue": "Visual",
|
95 |
+
"JPEG": "Compression",
|
96 |
+
"GaussianBlur": "Visual",
|
97 |
+
"MedianFilter": "Visual",
|
98 |
+
"H264": "Compression",
|
99 |
+
"H264rgb": "Compression",
|
100 |
+
"H265": "Compression",
|
101 |
+
"VP9": "Compression",
|
102 |
+
"H264_Crop_Brightness0": "Mixed",
|
103 |
+
"H264_Crop_Brightness1": "Mixed",
|
104 |
+
"H264_Crop_Brightness2": "Mixed",
|
105 |
+
"H264_Crop_Brightness3": "Mixed",
|
106 |
+
},
|
107 |
+
}
|
108 |
+
else:
|
109 |
+
raise ValueError(f"Unknown dataset: {dataset_name}")
|
frontend/src/App.tsx
CHANGED
@@ -1,14 +1,11 @@
|
|
1 |
import { useState } from 'react'
|
2 |
-
import API from './API'
|
3 |
-
import DataChart from './components/DataChart'
|
4 |
-
import LeaderboardTable from './components/LeaderboardTable'
|
5 |
import Examples from './components/Examples'
|
|
|
6 |
|
7 |
function App() {
|
8 |
-
const file = 'voxpopuli_1k_audio'
|
9 |
const [activeTab, setActiveTab] = useState<
|
10 |
-
'
|
11 |
-
>('
|
12 |
|
13 |
return (
|
14 |
<div className="min-h-screen w-11/12 mx-auto">
|
@@ -17,20 +14,8 @@ function App() {
|
|
17 |
<h2 className="card-title">🥇 Omni Seal Bench Watermarking Leaderboard</h2>
|
18 |
</div>
|
19 |
</div>
|
20 |
-
<div className="tabs tabs-border">
|
21 |
-
<input
|
22 |
-
type="radio"
|
23 |
-
name="my_tabs_6"
|
24 |
-
className="tab"
|
25 |
-
aria-label="Data Chart"
|
26 |
-
checked={activeTab === 'dataChart'}
|
27 |
-
onChange={() => setActiveTab('dataChart')}
|
28 |
-
defaultChecked
|
29 |
-
/>
|
30 |
-
<div className="tab-content bg-base-100 border-base-300 p-6">
|
31 |
-
<DataChart file={file} />
|
32 |
-
</div>
|
33 |
|
|
|
34 |
<input
|
35 |
type="radio"
|
36 |
name="my_tabs_6"
|
@@ -40,7 +25,7 @@ function App() {
|
|
40 |
onChange={() => setActiveTab('leaderboard')}
|
41 |
/>
|
42 |
<div className="tab-content bg-base-100 border-base-300 p-6">
|
43 |
-
<
|
44 |
</div>
|
45 |
|
46 |
<input
|
|
|
1 |
import { useState } from 'react'
|
|
|
|
|
|
|
2 |
import Examples from './components/Examples'
|
3 |
+
import LeaderBoardPage from './components/LeaderBoardPage'
|
4 |
|
5 |
function App() {
|
|
|
6 |
const [activeTab, setActiveTab] = useState<
|
7 |
+
'leaderboard' | 'imageExamples' | 'audioExamples' | 'videoExamples'
|
8 |
+
>('leaderboard')
|
9 |
|
10 |
return (
|
11 |
<div className="min-h-screen w-11/12 mx-auto">
|
|
|
14 |
<h2 className="card-title">🥇 Omni Seal Bench Watermarking Leaderboard</h2>
|
15 |
</div>
|
16 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
+
<div className="tabs tabs-border">
|
19 |
<input
|
20 |
type="radio"
|
21 |
name="my_tabs_6"
|
|
|
25 |
onChange={() => setActiveTab('leaderboard')}
|
26 |
/>
|
27 |
<div className="tab-content bg-base-100 border-base-300 p-6">
|
28 |
+
<LeaderBoardPage />
|
29 |
</div>
|
30 |
|
31 |
<input
|
frontend/src/components/DataChart.tsx
CHANGED
@@ -12,7 +12,7 @@ import {
|
|
12 |
import API from '../API'
|
13 |
|
14 |
interface DataChartProps {
|
15 |
-
|
16 |
}
|
17 |
|
18 |
interface Row {
|
@@ -76,7 +76,7 @@ const AttackSelector = ({
|
|
76 |
)
|
77 |
}
|
78 |
|
79 |
-
const DataChart = ({
|
80 |
const [chartData, setChartData] = useState<Row[]>([])
|
81 |
const [loading, setLoading] = useState(true)
|
82 |
const [error, setError] = useState<string | null>(null)
|
@@ -86,7 +86,7 @@ const DataChart = ({ file }: DataChartProps) => {
|
|
86 |
const [selectedAttack, setSelectedAttack] = useState<string | null>(null)
|
87 |
|
88 |
useEffect(() => {
|
89 |
-
API.fetchStaticFile(`data/${
|
90 |
.then((response) => {
|
91 |
const data = JSON.parse(response)
|
92 |
const rows: Row[] = data['all_attacks_df'].map((row: any) => {
|
|
|
12 |
import API from '../API'
|
13 |
|
14 |
interface DataChartProps {
|
15 |
+
dataset: string
|
16 |
}
|
17 |
|
18 |
interface Row {
|
|
|
76 |
)
|
77 |
}
|
78 |
|
79 |
+
const DataChart = ({ dataset }: DataChartProps) => {
|
80 |
const [chartData, setChartData] = useState<Row[]>([])
|
81 |
const [loading, setLoading] = useState(true)
|
82 |
const [error, setError] = useState<string | null>(null)
|
|
|
86 |
const [selectedAttack, setSelectedAttack] = useState<string | null>(null)
|
87 |
|
88 |
useEffect(() => {
|
89 |
+
API.fetchStaticFile(`data/${dataset}_attacks_variations`)
|
90 |
.then((response) => {
|
91 |
const data = JSON.parse(response)
|
92 |
const rows: Row[] = data['all_attacks_df'].map((row: any) => {
|
frontend/src/components/DatasetSelector.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react'
|
2 |
+
|
3 |
+
interface DatasetSelectorProps {
|
4 |
+
datasets: string[]
|
5 |
+
selectedDataset: string
|
6 |
+
onDatasetChange: (dataset: string) => void
|
7 |
+
}
|
8 |
+
|
9 |
+
const DatasetSelector: React.FC<DatasetSelectorProps> = ({
|
10 |
+
datasets,
|
11 |
+
selectedDataset,
|
12 |
+
onDatasetChange,
|
13 |
+
}) => {
|
14 |
+
return (
|
15 |
+
<div className="mb-4">
|
16 |
+
<fieldset className="fieldset w-full p-4 rounded border">
|
17 |
+
<legend className="fieldset-legend font-semibold">Dataset</legend>
|
18 |
+
<div className="flex flex-wrap gap-2">
|
19 |
+
{datasets.map((dataset) => (
|
20 |
+
<label key={dataset} className="flex items-center gap-2 cursor-pointer">
|
21 |
+
<input
|
22 |
+
type="radio"
|
23 |
+
name="dataset"
|
24 |
+
className="radio radio-sm"
|
25 |
+
checked={selectedDataset === dataset}
|
26 |
+
onChange={() => onDatasetChange(dataset)}
|
27 |
+
/>
|
28 |
+
<span className="text-sm">{dataset}</span>
|
29 |
+
</label>
|
30 |
+
))}
|
31 |
+
</div>
|
32 |
+
</fieldset>
|
33 |
+
</div>
|
34 |
+
)
|
35 |
+
}
|
36 |
+
|
37 |
+
export default DatasetSelector
|
frontend/src/components/LeaderBoardPage.tsx
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react'
|
2 |
+
import DatasetSelector from './DatasetSelector'
|
3 |
+
import LeaderboardTable from './LeaderboardTable'
|
4 |
+
import DataChart from './DataChart'
|
5 |
+
|
6 |
+
const LeaderBoardPage: React.FC = () => {
|
7 |
+
const datasets = [
|
8 |
+
'voxpopuli_1k_audio',
|
9 |
+
'ravdess_1k_audio',
|
10 |
+
'val2014_1k_image',
|
11 |
+
'sav_val_full_video',
|
12 |
+
]
|
13 |
+
const [selectedDataset, setSelectedDataset] = useState('voxpopuli_1k_audio')
|
14 |
+
|
15 |
+
return (
|
16 |
+
<div className="space-y-6">
|
17 |
+
<DatasetSelector
|
18 |
+
datasets={datasets}
|
19 |
+
selectedDataset={selectedDataset}
|
20 |
+
onDatasetChange={setSelectedDataset}
|
21 |
+
/>
|
22 |
+
|
23 |
+
<div className="space-y-8">
|
24 |
+
<LeaderboardTable dataset={selectedDataset} />
|
25 |
+
<div className="mt-8 pt-4 border-t border-gray-200">
|
26 |
+
<h3 className="text-lg font-semibold mb-4">Performance Chart</h3>
|
27 |
+
<DataChart dataset={selectedDataset} />
|
28 |
+
</div>
|
29 |
+
</div>
|
30 |
+
</div>
|
31 |
+
)
|
32 |
+
}
|
33 |
+
|
34 |
+
export default LeaderBoardPage
|
frontend/src/components/LeaderboardTable.tsx
CHANGED
@@ -4,7 +4,7 @@ import LeaderboardFilter from './LeaderboardFilter'
|
|
4 |
import ModelFilter from './ModelFilter'
|
5 |
|
6 |
interface LeaderboardTableProps {
|
7 |
-
|
8 |
}
|
9 |
|
10 |
interface Row {
|
@@ -21,7 +21,7 @@ interface GroupStats {
|
|
21 |
stdDev: { [key: string]: number }
|
22 |
}
|
23 |
|
24 |
-
const LeaderboardTable: React.FC<LeaderboardTableProps> = ({
|
25 |
const [tableRows, setTableRows] = useState<Row[]>([])
|
26 |
const [tableHeader, setTableHeader] = useState<string[]>([])
|
27 |
const [loading, setLoading] = useState(true)
|
@@ -39,7 +39,7 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
|
39 |
const [overallMetrics, setOverallMetrics] = useState<string[]>([])
|
40 |
|
41 |
useEffect(() => {
|
42 |
-
API.fetchStaticFile(`data/${
|
43 |
.then((response) => {
|
44 |
const data = JSON.parse(response)
|
45 |
const rows: Row[] = data['rows']
|
@@ -128,7 +128,7 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
|
128 |
setError('Failed to fetch JSON: ' + err.message)
|
129 |
setLoading(false)
|
130 |
})
|
131 |
-
}, [
|
132 |
|
133 |
const toggleGroup = (group: string) => {
|
134 |
setOpenGroups((prev) => ({ ...prev, [group]: !prev[group] }))
|
@@ -228,7 +228,6 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
|
228 |
|
229 |
return (
|
230 |
<div className="rounded shadow overflow-auto">
|
231 |
-
<h3 className="font-bold mb-2">{file}</h3>
|
232 |
{loading && <div>Loading...</div>}
|
233 |
{error && <div className="text-red-500">{error}</div>}
|
234 |
|
|
|
4 |
import ModelFilter from './ModelFilter'
|
5 |
|
6 |
interface LeaderboardTableProps {
|
7 |
+
dataset: string
|
8 |
}
|
9 |
|
10 |
interface Row {
|
|
|
21 |
stdDev: { [key: string]: number }
|
22 |
}
|
23 |
|
24 |
+
const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ dataset }) => {
|
25 |
const [tableRows, setTableRows] = useState<Row[]>([])
|
26 |
const [tableHeader, setTableHeader] = useState<string[]>([])
|
27 |
const [loading, setLoading] = useState(true)
|
|
|
39 |
const [overallMetrics, setOverallMetrics] = useState<string[]>([])
|
40 |
|
41 |
useEffect(() => {
|
42 |
+
API.fetchStaticFile(`data/${dataset}_benchmark`)
|
43 |
.then((response) => {
|
44 |
const data = JSON.parse(response)
|
45 |
const rows: Row[] = data['rows']
|
|
|
128 |
setError('Failed to fetch JSON: ' + err.message)
|
129 |
setLoading(false)
|
130 |
})
|
131 |
+
}, [dataset])
|
132 |
|
133 |
const toggleGroup = (group: string) => {
|
134 |
setOpenGroups((prev) => ({ ...prev, [group]: !prev[group] }))
|
|
|
228 |
|
229 |
return (
|
230 |
<div className="rounded shadow overflow-auto">
|
|
|
231 |
{loading && <div>Loading...</div>}
|
232 |
{error && <div className="text-red-500">{error}</div>}
|
233 |
|