Skip to content

Commit 3885680

Browse files
authored
Merge pull request DataRecce#256 from DataRecce/feature/drc-332-change-the-behavior-of-state-loading-and-exporting
[Feature] Change the behavior of state loading and exporting
2 parents cc76c35 + 0fef1b5 commit 3885680

File tree

6 files changed

+69
-35
lines changed

6 files changed

+69
-35
lines changed

js/app/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { InfoIcon } from "@chakra-ui/icons";
3030
import { RunPage } from "@/components/run/RunPage";
3131
import { ErrorBoundary } from "@/components/errorboundary/ErrorBoundary";
3232
import { StateExporter } from "@/components/check/StateExporter";
33-
import { StateLoader } from "@/components/check/StateLoader";
33+
import { StateImporter } from "@/components/check/StateImporter";
3434

3535
function getCookie(key: string) {
3636
var b = document.cookie.match("(^|;)\\s*" + key + "\\s*=\\s*([^;]+)");
@@ -120,8 +120,8 @@ function NavBar() {
120120
<Spacer />
121121
{!isDemoSite && (
122122
<>
123+
<StateImporter />
123124
<StateExporter />
124-
<StateLoader />
125125
</>
126126
)}
127127
<Box p="8px" mr="10px" color="gray.500">

js/src/components/check/StateExporter.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { exportState } from "@/lib/api/state";
2-
import { DownloadIcon } from "@chakra-ui/icons";
3-
import { IconButton, Tooltip, useToast } from "@chakra-ui/react";
2+
import { Icon, IconButton, Tooltip, useToast } from "@chakra-ui/react";
43
import { format } from "date-fns";
54
import saveAs from "file-saver";
5+
import { TfiExport } from "react-icons/tfi";
66

77
export function StateExporter() {
88
const toast = useToast();
@@ -37,7 +37,7 @@ export function StateExporter() {
3737
variant="unstyled"
3838
aria-label="Export state"
3939
onClick={handleExport}
40-
icon={<DownloadIcon />}
40+
icon={<Icon as={TfiExport} boxSize={"1.2em"} />}
4141
/>
4242
</Tooltip>
4343
);

js/src/components/check/StateLoader.tsx js/src/components/check/StateImporter.tsx

+18-18
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ import {
1818
useToast,
1919
} from "@chakra-ui/react";
2020
import { InfoIcon } from "@chakra-ui/icons";
21-
import { loadState } from "@/lib/api/state";
22-
import { IoFolderOpenOutline } from "react-icons/io5";
21+
import { importState } from "@/lib/api/state";
2322
import { useLocation } from "wouter";
2423
import { useRunsAggregated } from "@/lib/hooks/LineageGraphContext";
24+
import { TfiImport } from "react-icons/tfi";
2525

26-
export function StateLoader() {
26+
export function StateImporter() {
2727
const toast = useToast();
2828
const queryClient = useQueryClient();
2929
const hiddenFileInput = useRef<HTMLInputElement>(null);
@@ -33,30 +33,30 @@ export function StateLoader() {
3333
const [location, setLocation] = useLocation();
3434
const [, refetchRunsAggregated] = useRunsAggregated();
3535

36-
const handleLoad = useCallback(async () => {
36+
const handleImport = useCallback(async () => {
3737
if (!selectedFile) {
3838
return;
3939
}
4040

4141
try {
42-
const { runs, checks } = await loadState(selectedFile);
42+
const { runs, checks } = await importState(selectedFile);
4343
refetchRunsAggregated();
4444
await queryClient.invalidateQueries({ queryKey: cacheKeys.checks() });
4545
if (location.includes("/checks")) {
4646
setLocation("/checks");
4747
}
4848
toast({
49-
description: `${runs} runs and ${checks} checks loaded successfully`,
49+
description: `${runs} runs and ${checks} checks imported successfully`,
5050
status: "info",
5151
variant: "left-accent",
5252
position: "bottom",
5353
duration: 5000,
5454
isClosable: true,
5555
});
5656
} catch (error) {
57-
console.error("Load failed", error);
57+
console.error("Import failed", error);
5858
toast({
59-
title: "Load failed",
59+
title: "Import failed",
6060
description: `${error}`,
6161
status: "error",
6262
variant: "left-accent",
@@ -92,12 +92,12 @@ export function StateLoader() {
9292

9393
return (
9494
<>
95-
<Tooltip label="Load">
95+
<Tooltip label="Import">
9696
<IconButton
9797
variant="unstyled"
98-
aria-label="Load state"
98+
aria-label="Import state"
9999
onClick={handleClick}
100-
icon={<Icon pt="10px" as={IoFolderOpenOutline} boxSize={"2em"} />}
100+
icon={<Icon as={TfiImport} boxSize={"1.2em"} />}
101101
/>
102102
</Tooltip>
103103
<input
@@ -110,12 +110,12 @@ export function StateLoader() {
110110
isOpen={isOpen}
111111
leastDestructiveRef={cancelRef}
112112
onClose={onClose}
113-
size={"lg"}
113+
size={"xl"}
114114
>
115115
<AlertDialogOverlay>
116116
<AlertDialogContent>
117117
<AlertDialogHeader fontSize="lg" fontWeight="bold">
118-
Load state
118+
Import state
119119
</AlertDialogHeader>
120120

121121
<AlertDialogBody>
@@ -128,11 +128,11 @@ export function StateLoader() {
128128
</Flex>
129129
<Flex>
130130
<Text>
131-
All runs and checks will be{" "}
131+
The current runs and checks will be{" "}
132132
<Text as="span" fontWeight="600">
133-
overwritten
133+
merged
134134
</Text>{" "}
135-
by the loaded state
135+
with the imported state
136136
</Text>
137137
</Flex>
138138
</Flex>
@@ -142,8 +142,8 @@ export function StateLoader() {
142142
<Button ref={cancelRef} onClick={onClose}>
143143
Cancel
144144
</Button>
145-
<Button colorScheme="blue" onClick={handleLoad} ml="5px">
146-
Load
145+
<Button colorScheme="blue" onClick={handleImport} ml="5px">
146+
Import
147147
</Button>
148148
</AlertDialogFooter>
149149
</AlertDialogContent>

js/src/lib/api/state.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { axiosClient } from "./axiosClient";
22

3-
export interface LoadedState {
3+
export interface ImportedState {
44
runs: number;
55
checks: number;
66
}
@@ -10,10 +10,10 @@ export async function exportState(): Promise<string> {
1010
return response.data;
1111
}
1212

13-
export async function loadState(file: File): Promise<LoadedState> {
13+
export async function importState(file: File): Promise<ImportedState> {
1414
const formData = new FormData();
1515
formData.append("file", file);
1616

17-
const response = await axiosClient.post("/api/load", formData);
17+
const response = await axiosClient.post("/api/import", formData);
1818
return response.data;
1919
}

recce/models/state.py

+27
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,33 @@ def load(cls, file_path):
6262

6363
return state
6464

65+
def export_state(self):
66+
self.metadata = RecceStateMetadata()
67+
return pydantic_model_json_dump(self)
68+
69+
def import_state(self, content):
70+
import_state = RecceState.model_validate_json(content)
71+
current_check_ids = [str(c.check_id) for c in self.checks]
72+
current_run_ids = [str(r.run_id) for r in self.runs]
73+
74+
# merge checks
75+
import_checks = 0
76+
for check in import_state.checks:
77+
if str(check.check_id) not in current_check_ids:
78+
self.checks.append(check)
79+
import_checks += 1
80+
81+
# merge runs
82+
import_runs = 0
83+
for run in import_state.runs:
84+
if str(run.run_id) not in current_run_ids:
85+
self.runs.append(run)
86+
import_runs += 1
87+
88+
self.runs.sort(key=lambda x: x.run_at)
89+
90+
return import_runs, import_checks
91+
6592

6693
recce_state: Optional[RecceState] = None
6794

recce/server.py

+16-9
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
from .apis.run_api import run_router
2121
from .dbt import load_dbt_context, default_dbt_context
2222
from .exceptions import RecceException
23+
from .models.lineage import LineageDAO
2324
from .models.state import load_default_state, default_recce_state, RecceState
24-
from .models.util import pydantic_model_json_dump
25+
from .models.types import Lineage
2526

2627
logger = logging.getLogger('uvicorn')
2728

@@ -124,21 +125,27 @@ async def get_lineage(base: Optional[bool] = False):
124125
@app.post("/api/export", response_class=PlainTextResponse, status_code=200)
125126
async def export_handler():
126127
try:
127-
return pydantic_model_json_dump(default_recce_state())
128+
ctx = default_dbt_context()
129+
base = ctx.get_lineage(base=True)
130+
current = ctx.get_lineage(base=False)
131+
lineage = Lineage(
132+
base=base,
133+
current=current,
134+
)
135+
LineageDAO().set(lineage)
136+
137+
return default_recce_state().export_state()
128138
except RecceException as e:
129139
raise HTTPException(status_code=400, detail=e.message)
130140

131141

132-
@app.post("/api/load", status_code=200)
133-
async def load_handler(file: UploadFile):
142+
@app.post("/api/import", status_code=200)
143+
async def import_handler(file: UploadFile):
134144
try:
135145
content = await file.read()
136-
load_state = RecceState().model_validate_json(content)
137-
current_state = default_recce_state()
138-
current_state.checks = load_state.checks
139-
current_state.runs = load_state.runs
146+
import_runs, import_checks = default_recce_state().import_state(content)
140147

141-
return {"runs": len(current_state.runs), "checks": len(current_state.checks)}
148+
return {"runs": import_runs, "checks": import_checks}
142149
except ValidationError as e:
143150
raise HTTPException(status_code=400, detail=str(e))
144151
except RecceException as e:

0 commit comments

Comments
 (0)