Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,64 @@ import {
ModalHeader,
ModalOverlay,
Text as ChakraText,
useToast,
} from '@chakra-ui/react';
import React, { useState } from 'react';
import { useAppDispatch } from '../../app/hooks';
import { StyledDropzone } from '../../common/StyledDropzone';
import { isValidJsonFile } from '../../common/util/validation';
import { AnnotationStore, initialAnnotationStore, mergeAnnotationStore, setAnnotationStore } from './annotationSlice';
import {
AnnotationStore,
EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION,
initialAnnotationStore,
mergeAnnotationStore,
setAnnotationStore,
VersionedAnnotationStore,
} from './annotationSlice';
import { hideAnnotationImportDialog, toggleAnnotationImportDialog } from '../ui/uiSlice';

export const AnnotationImportDialog: React.FC = function () {
const toast = useToast();
const [fileName, setFileName] = useState('');
const [newAnnotationStore, setNewAnnotationStore] = useState<AnnotationStore>(initialAnnotationStore);
const [newAnnotationStore, setNewAnnotationStore] = useState<VersionedAnnotationStore>(initialAnnotationStore);
const dispatch = useAppDispatch();

const validate = () => {
if (!fileName) {
toast({
title: 'No File Selected',
description: 'Select a file to import or cancel this dialog.',
status: 'error',
duration: 4000,
});
return false;
}

if ((newAnnotationStore.schemaVersion ?? 1) !== EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION) {
toast({
title: 'Old Annotation File',
description: 'This file is not compatible with the current version of the API Editor.',
status: 'error',
duration: 4000,
});
return false;
}

return true;
};
const merge = () => {
if (fileName) {
if (validate()) {
delete newAnnotationStore.schemaVersion;
dispatch(mergeAnnotationStore(newAnnotationStore));
dispatch(hideAnnotationImportDialog());
}
dispatch(hideAnnotationImportDialog());
};
const replace = () => {
if (fileName) {
if (validate()) {
delete newAnnotationStore.schemaVersion;
dispatch(setAnnotationStore(newAnnotationStore));
dispatch(hideAnnotationImportDialog());
}
dispatch(hideAnnotationImportDialog());
};
const close = () => dispatch(toggleAnnotationImportDialog());

Expand Down
16 changes: 14 additions & 2 deletions api-editor/gui/src/features/annotations/annotationSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import * as idb from 'idb-keyval';
import { RootState } from '../../app/store';
import { isValidUsername } from '../../common/util/validation';

export const EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION = 1;
export const EXPECTED_ANNOTATION_SLICE_SCHEMA_VERSION = 1;

/**
* How many annotations can be applied to a class at once.
*/
Expand All @@ -11,16 +14,17 @@ export const maximumNumberOfClassAnnotations = 5;
/**
* How many annotations can be applied to a function at once.
*/
export const maximumNumberOfFunctionAnnotations = 7;
export const maximumNumberOfFunctionAnnotations = 8;

/**
* How many annotations can be applied to a parameter at once.
*/
export const maximumNumberOfParameterAnnotations = 8;
export const maximumNumberOfParameterAnnotations = 9;

const maximumUndoHistoryLength = 10;

export interface AnnotationSlice {
schemaVersion?: number;
annotations: AnnotationStore;
queue: AnnotationStore[];
queueIndex: number;
Expand Down Expand Up @@ -75,6 +79,10 @@ export interface AnnotationStore {
};
}

export interface VersionedAnnotationStore extends AnnotationStore {
schemaVersion?: number;
}

export interface Annotation {
/**
* ID of the annotated Python declaration.
Expand Down Expand Up @@ -301,6 +309,10 @@ export const initialAnnotationSlice: AnnotationSlice = {
export const initializeAnnotations = createAsyncThunk('annotations/initialize', async () => {
try {
const storedAnnotations = (await idb.get('annotations')) as AnnotationSlice;
if ((storedAnnotations.schemaVersion ?? 1) !== EXPECTED_ANNOTATION_SLICE_SCHEMA_VERSION) {
return initialAnnotationSlice;
}

return {
...initialAnnotationSlice,
...storedAnnotations,
Expand Down
3 changes: 2 additions & 1 deletion api-editor/gui/src/features/menuBar/HelpMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button, Icon, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
import { Box, Button, Icon, Menu, MenuButton, MenuDivider, MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
import React from 'react';
import { FaBug, FaChevronDown, FaLightbulb } from 'react-icons/fa';
import { bugReportURL, featureRequestURL, userGuideURL } from '../externalLinks/urlBuilder';
Expand All @@ -21,6 +21,7 @@ export const HelpMenu = function () {
User Guide
</MenuItem>
</MenuGroup>
<MenuDivider />
<MenuGroup title="Feedback">
<MenuItem
paddingLeft={8}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ModalHeader,
ModalOverlay,
Text as ChakraText,
useToast,
} from '@chakra-ui/react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
Expand All @@ -25,23 +26,43 @@ import { persistPythonPackage, setPythonPackage } from './apiSlice';
import { resetUsages } from '../usages/usageSlice';

export const PackageDataImportDialog: React.FC = function () {
const toast = useToast();
const [fileName, setFileName] = useState('');
const [newPythonPackage, setNewPythonPackage] = useState<string>();
const [newPythonPackageString, setNewPythonPackageString] = useState<string>();
const navigate = useNavigate();
const dispatch = useAppDispatch();

const submit = async () => {
if (newPythonPackage) {
const parsedPythonPackage = JSON.parse(newPythonPackage) as PythonPackageJson;
if (!fileName) {
toast({
title: 'No File Selected',
description: 'Select a file to import or cancel this dialog.',
status: 'error',
duration: 4000,
});
return;
}

dispatch(setPythonPackage(parsePythonPackageJson(parsedPythonPackage)));
dispatch(persistPythonPackage(parsedPythonPackage));
if (newPythonPackageString) {
const pythonPackageJson = JSON.parse(newPythonPackageString) as PythonPackageJson;
const pythonPackage = parsePythonPackageJson(pythonPackageJson);
if (pythonPackage) {
dispatch(setPythonPackage(pythonPackage));
dispatch(persistPythonPackage(pythonPackageJson));

// Reset other slices
dispatch(resetAnnotationStore());
dispatch(resetUsages());
dispatch(resetUIAfterAPIImport());
navigate('/');
// Reset other slices
dispatch(resetAnnotationStore());
dispatch(resetUsages());
dispatch(resetUIAfterAPIImport());
navigate('/');
} else {
toast({
title: 'Old API File',
description: 'This file is not compatible with the current version of the API Editor.',
status: 'error',
duration: 4000,
});
}
}
};
const close = () => dispatch(toggleAPIImportDialog());
Expand All @@ -56,7 +77,7 @@ export const PackageDataImportDialog: React.FC = function () {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === 'string') {
setNewPythonPackage(reader.result);
setNewPythonPackageString(reader.result);
dispatch(resetAnnotationStore());
}
};
Expand Down
24 changes: 22 additions & 2 deletions api-editor/gui/src/features/packageData/apiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,43 @@ import { selectUsages } from '../usages/usageSlice';
import { selectAnnotationStore } from '../annotations/annotationSlice';
import { PythonDeclaration } from './model/PythonDeclaration';

export const EXPECTED_API_SCHEMA_VERSION = 1;

export interface APIState {
pythonPackage: PythonPackage;
}

// Initial state -------------------------------------------------------------------------------------------------------

const initialPythonPackageJson: PythonPackageJson = {
schemaVersion: EXPECTED_API_SCHEMA_VERSION,
distribution: 'empty',
package: 'empty',
version: '0.0.1',
modules: [],
classes: [],
functions: [],
};

const initialPythonPackage = new PythonPackage('empty', 'empty', '0.0.1');

const initialState: APIState = {
pythonPackage: new PythonPackage('empty', 'empty', '0.0.1'),
pythonPackage: initialPythonPackage,
};

// Thunks --------------------------------------------------------------------------------------------------------------

export const initializePythonPackage = createAsyncThunk('api/initialize', async () => {
try {
const storedPythonPackageJson = (await idb.get('api')) as PythonPackageJson;
const pythonPackage = parsePythonPackageJson(storedPythonPackageJson);
if (!pythonPackage) {
await idb.set('api', initialPythonPackageJson);
return initialState;
}

return {
pythonPackage: parsePythonPackageJson(storedPythonPackageJson),
pythonPackage,
};
} catch {
return initialState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { PythonPackage } from './PythonPackage';
import { PythonParameter, PythonParameterAssignment } from './PythonParameter';
import { PythonResult } from './PythonResult';
import { PythonDeclaration } from './PythonDeclaration';
import { EXPECTED_API_SCHEMA_VERSION } from '../apiSlice';

export interface PythonPackageJson {
schemaVersion?: number;
distribution: string;
package: string;
version: string;
Expand All @@ -18,7 +20,11 @@ export interface PythonPackageJson {
functions: PythonFunctionJson[];
}

export const parsePythonPackageJson = function (packageJson: PythonPackageJson): PythonPackage {
export const parsePythonPackageJson = function (packageJson: PythonPackageJson): PythonPackage | null {
if ((packageJson.schemaVersion ?? 1) !== EXPECTED_API_SCHEMA_VERSION) {
return null;
}

const idToDeclaration = new Map();

// Functions
Expand Down
7 changes: 7 additions & 0 deletions api-editor/gui/src/features/ui/uiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import { PythonDeclaration } from '../packageData/model/PythonDeclaration';
import { UsageCountStore } from '../usages/model/UsageCountStore';
import { selectUsages } from '../usages/usageSlice';

const EXPECTED_UI_SCHEMA_VERSION = 1;

export interface Filter {
filter: string;
name: string;
}

export interface UIState {
schemaVersion?: number;
showAnnotationImportDialog: boolean;
showAPIImportDialog: boolean;
showUsageImportDialog: boolean;
Expand Down Expand Up @@ -162,6 +165,10 @@ export const initialState: UIState = {
export const initializeUI = createAsyncThunk('ui/initialize', async () => {
try {
const storedState = (await idb.get('ui')) as UIState;
if ((storedState.schemaVersion ?? 1) !== EXPECTED_UI_SCHEMA_VERSION) {
return initialState;
}

return {
...initialState,
...storedState,
Expand Down
26 changes: 24 additions & 2 deletions api-editor/gui/src/features/usages/UsageImportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ModalHeader,
ModalOverlay,
Text as ChakraText,
useToast,
} from '@chakra-ui/react';
import React, { useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../app/hooks';
Expand All @@ -23,17 +24,38 @@ import { setUsages } from './usageSlice';
import { selectRawPythonPackage } from '../packageData/apiSlice';

export const UsageImportDialog: React.FC = function () {
const toast = useToast();
const [fileName, setFileName] = useState('');
const [newUsages, setNewUsages] = useState<string>();
const dispatch = useAppDispatch();
const api = useAppSelector(selectRawPythonPackage);

const submit = async () => {
if (!fileName) {
toast({
title: 'No File Selected',
description: 'Select a file to import or cancel this dialog.',
status: 'error',
duration: 4000,
});
return;
}

if (newUsages) {
const parsedUsages = JSON.parse(newUsages) as UsageCountJson;
dispatch(setUsages(UsageCountStore.fromJson(parsedUsages, api)));
const usageCountStore = UsageCountStore.fromJson(parsedUsages, api);
if (usageCountStore) {
dispatch(setUsages(usageCountStore));
close();
} else {
toast({
title: 'Old Usage Count File',
description: 'This file is not compatible with the current version of the API Editor.',
status: 'error',
duration: 4000,
});
}
}
close();
};
const close = () => dispatch(toggleUsageImportDialog());

Expand Down
10 changes: 9 additions & 1 deletion api-editor/gui/src/features/usages/model/UsageCountStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { PythonModule } from '../../packageData/model/PythonModule';
import { PythonClass } from '../../packageData/model/PythonClass';
import { PythonFunction } from '../../packageData/model/PythonFunction';

export const EXPECTED_USAGES_SCHEMA_VERSION = 1;

export interface UsageCountJson {
schemaVersion?: number;
module_counts?: {
[target: string]: number;
};
Expand All @@ -26,7 +29,11 @@ export interface UsageCountJson {
}

export class UsageCountStore {
static fromJson(json: UsageCountJson, api?: PythonPackage): UsageCountStore {
static fromJson(json: UsageCountJson, api?: PythonPackage): UsageCountStore | null {
if ((json.schemaVersion ?? 1) !== EXPECTED_USAGES_SCHEMA_VERSION) {
return null;
}

return new UsageCountStore(
new Map(Object.entries(json.module_counts ?? {})),
new Map(Object.entries(json.class_counts)),
Expand Down Expand Up @@ -86,6 +93,7 @@ export class UsageCountStore {

toJson(): UsageCountJson {
return {
schemaVersion: EXPECTED_USAGES_SCHEMA_VERSION,
module_counts: Object.fromEntries(this.moduleUsages),
class_counts: Object.fromEntries(this.classUsages),
function_counts: Object.fromEntries(this.functionUsages),
Expand Down
2 changes: 1 addition & 1 deletion api-editor/gui/src/features/usages/usageSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const initializeUsages = createAsyncThunk('usages/initialize', async () =
try {
const storedUsageCountStoreJson = (await idb.get('usages')) as UsageCountJson;
return {
usages: UsageCountStore.fromJson(storedUsageCountStoreJson),
usages: UsageCountStore.fromJson(storedUsageCountStoreJson) ?? new UsageCountStore(),
};
} catch {
return initialState;
Expand Down
Loading