Skip to content
Snippets Groups Projects
Commit 308502ac authored by Leah Tacke genannt Unterberg's avatar Leah Tacke genannt Unterberg
Browse files

work on MitMDataset list view frontend

parent bec13654
No related branches found
No related tags found
No related merge requests found
Showing
with 420 additions and 16 deletions
......@@ -31,7 +31,7 @@ import { t, styled } from '@superset-ui/core';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import Fieldset from './Fieldset';
import { recurseReactClone } from './utils';
import { recurseReactClone } from '../utils';
interface CRUDCollectionProps {
allowAddItem?: boolean;
......
......@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, PureComponent } from 'react';
import { PureComponent, ReactNode } from 'react';
import { Form } from 'src/components/Form';
import { recurseReactClone } from './utils';
import { recurseReactClone } from '../utils';
import Field from './Field';
interface FieldsetProps {
......
import { FunctionComponent, useEffect, useState } from 'react';
import { MITM } from '../types';
import withToasts from '../../../components/MessageToasts/withToasts';
import Modal from '../../../components/Modal';
import { ClientErrorObject, SupersetTheme, t } from '@superset-ui/core';
import {
antDModalNoPaddingStyles,
antDModalStyles,
formStyles,
} from '../../databases/UploadDataModel/styles';
import { Form, Input, Select, Upload } from 'antd-v5';
import { AsyncActionResult, useExternalService } from '../asyncExternalService';
import { Button } from '../../../components';
import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
interface UploadMitMDatasetModalProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
onHide: () => void;
show: boolean;
allowedExtensions: string[];
mitm: MITM;
}
interface UploadMitMDatasetRequestFormData {
mitm: MITM;
datasetName: string;
mitmZip: File | Blob;
}
const UploadMitMDatasetModal: FunctionComponent<
UploadMitMDatasetModalProps
> = ({ addDangerToast, addSuccessToast, onHide, show, allowedExtensions }) => {
const { asyncAction } = useExternalService();
// const [mitm, setMitm] = useState<MITM | null>(null);
// const [datasetName, setDatasetName] = useState<string | null>(null);
// const [fileList, setFileList] = useState<UploadFile[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<ClientErrorObject>();
const [form] = Form.useForm();
const onSuccess = (
r: AsyncActionResult,
{
mitm,
datasetName,
}: Pick<UploadMitMDatasetRequestFormData, 'mitm' | 'datasetName'>,
) => {
addSuccessToast(t('Successfully uploaded \"%s\" (%s)', datasetName, mitm));
};
const onFailure = (error: ClientErrorObject) => {
setError(error);
};
useEffect(() => {
addDangerToast(t('An error occurred when attempting to upload: %s', error));
}, [error]);
const clearModal = () => {
// setDatasetName(null);
// setMitm(null);
// setFileList([]);
setIsLoading(false);
form.resetFields();
};
const makeRequest = ({
datasetName,
mitm,
mitmZip,
}: UploadMitMDatasetRequestFormData) => {
const formData = new FormData();
formData.append('dataset_name', datasetName);
formData.append('mitm', String(mitm));
formData.append('mitm_zip', mitmZip);
setIsLoading(true);
asyncAction({
endpoint: '/api/v1/mitm_dataset/upload/',
headers: { 'Content-Type': 'multipart/form-data' },
postPayload: formData,
})
.then(r => {
console.log(r);
onSuccess(r, { datasetName, mitm });
})
.catch(onFailure)
.finally(() => setIsLoading(false));
};
const onFinish = (values: any) => {
const fields = form.getFieldsValue();
const mitmZip = fields.fileList[0]?.originFileObj;
const mitm = fields.mitm as MITM;
makeRequest({ datasetName: fields.datasetName, mitm, mitmZip });
};
const onClose = () => {
clearModal();
onHide();
};
const extractFileList = (e: any) => {
console.log('Upload event:', e);
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
};
return (
<Modal
css={(theme: SupersetTheme) => [
antDModalNoPaddingStyles,
antDModalStyles(theme),
formStyles(theme),
]}
primaryButtonLoading={isLoading}
name="database"
data-test="upload-modal"
onHandledPrimaryAction={form.submit}
onHide={onClose}
width="500px"
primaryButtonName="Upload"
centered
show={show}
title="Upload MitM Dataset"
>
<Form form={form} onFinish={onFinish}>
<Form.Item
name="datasetName"
label="Dataset Name"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="mitm"
label="MitM Name"
rules={[{ required: true }, { enum: [MITM.MAED] }]}
>
<Select
placeholder="Select the MitM Name"
allowClear
options={[{ name: 'MAED', value: 'MAED' }]}
></Select>
</Form.Item>
<Form.Item
name="upload"
label="Upload"
valuePropName="fileList"
getValueFromEvent={extractFileList}
extra="longgggggggggggggggggggggggggggggggggg"
>
<Upload
name="logo"
accept=".zip,.maed,.mitm"
customRequest={() => false}
>
<Button icon={<UploadOutlined />}>Click to upload</Button>
</Upload>
</Form.Item>
<Form.Item label="Dragger">
<Form.Item
name="dragger"
valuePropName="fileList"
getValueFromEvent={extractFileList}
noStyle
>
<Upload.Dragger name="files">
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">
Click or drag file to this area to upload
</p>
<p className="ant-upload-hint">
Support for a single or bulk upload.
</p>
</Upload.Dragger>
</Form.Item>
</Form.Item>
</Form>
</Modal>
);
};
export default withToasts(UploadMitMDatasetModal);
import {
ClientErrorObject,
getClientErrorObject,
JsonObject,
logging,
parseErrorJson,
RequestConfig,
SupersetClient,
SupersetError,
} from '@superset-ui/core';
import {useAsyncEventHandling} from "../../middleware/asyncEvent";
const extServiceCallApiEndpoint = '/api/v1/ext_service/call';
export enum ExternalService {
MitMService = 'mitm_service',
}
type AsyncJobMetadata = {
id?: string | null;
channel_id: string;
job_id: string;
user_id?: string;
status: string;
errors?: SupersetError[];
result_url: string | null;
task_id?: string; // my addition
};
const JOB_STATUS = {
PENDING: 'pending',
RUNNING: 'running',
ERROR: 'error',
DONE: 'done',
};
const fetchJsonResult = async (
asyncEvent: AsyncJobMetadata, // & { result_url: string }
): Promise<{
status: 'success' | 'error';
data: JsonObject | ClientErrorObject;
}> => {
try {
const { json } = await SupersetClient.get({
endpoint: String(asyncEvent.result_url),
});
return { status: 'success', data: 'result' in json ? json.result : json };
} catch (response) {
const err = await getClientErrorObject(response);
return { status: 'error', data: err };
}
};
export type AsyncActionResult = {
status: string;
lastEvent: AsyncJobMetadata;
};
const {addListener, removeListener} = useAsyncEventHandling()
export const waitForAsyncData = async (asyncResponse: AsyncJobMetadata) =>
new Promise<AsyncActionResult | JsonObject>((resolve, reject) => {
const jobId = asyncResponse.job_id;
const listener = async (asyncEvent: AsyncJobMetadata) => {
switch (asyncEvent.status) {
case JOB_STATUS.DONE: {
if ('result_url' in asyncEvent) {
let { data, status } = await fetchJsonResult(asyncEvent);
if (status === 'success') {
resolve(data);
} else {
reject(data);
}
} else {
resolve({
status: JOB_STATUS.DONE,
lastEvent: asyncEvent,
} as AsyncActionResult);
}
break;
}
case JOB_STATUS.ERROR: {
const err = parseErrorJson(asyncEvent);
reject(err);
break;
}
default: {
logging.warn('received event with status', asyncEvent.status);
}
}
removeListener(jobId);
};
addListener(jobId, listener);
});
export const asyncCall = async (requestConfig: RequestConfig) => {
return SupersetClient.post({
...requestConfig,
}).then(
({ response, json }) =>
new Promise<AsyncActionResult | JsonObject>((resolve, reject) => {
{
if (response.status === 202) {
const jm: AsyncJobMetadata = json;
waitForAsyncData(jm).then(resolve).catch(reject);
} else {
const err = parseErrorJson(json);
reject(err);
}
}
}),
);
};
export const asyncAction = async (
requestConfig: RequestConfig,
): Promise<AsyncActionResult> => {
return asyncCall(requestConfig);
};
type CallExternalServiceProps = {
extService: ExternalService;
path: string;
payload?: JsonObject | FormData | undefined;
};
export const callExternalService = async ({
extService,
path,
payload,
}: CallExternalServiceProps) => {
const endpoint =
extServiceCallApiEndpoint + '/' + String(extService) + '/' + path;
const contentType =
payload instanceof FormData ? 'multipart/form-data' : 'application/json';
return asyncCall({
endpoint,
headers: { 'Content-Type': contentType },
postPayload: payload,
});
};
export const useExternalService = () => {
return {asyncCall, asyncAction, callExternalService}
}
......@@ -25,3 +25,7 @@ export interface MitMDataset
slices?: Array<Slice>;
dashboards?: Array<Dashboard>;
}
export enum MITM {
MAED = 'MAED'
}
import {Children, cloneElement} from "react";
export function recurseReactClone(children, type, propExtender) {
/**
* Clones a React component's children, and injects new props
* where the type specified is matched.
*/
return Children.map(children, child => {
let newChild = child;
if (child && child.type.name === type.name) {
newChild = cloneElement(child, propExtender(child));
}
if (newChild && newChild.props.children) {
newChild = cloneElement(newChild, {
children: recurseReactClone(
newChild.props.children,
type,
propExtender,
),
});
}
return newChild;
});
}
......@@ -18,13 +18,13 @@
*/
import {
ensureIsArray,
isFeatureEnabled,
FeatureFlag,
makeApi,
SupersetClient,
logging,
getClientErrorObject,
isFeatureEnabled,
logging,
makeApi,
parseErrorJson,
SupersetClient,
SupersetError,
} from '@superset-ui/core';
import getBootstrapData from 'src/utils/getBootstrapData';
......@@ -76,6 +76,10 @@ const removeListener = (id: string) => {
delete listenersByJobId[id];
};
export const useAsyncEventHandling = () => {
return { addListener, removeListener };
};
const fetchCachedData = async (
asyncEvent: AsyncEvent,
): Promise<CachedDataResponse> => {
......
import {useParams} from "react-router-dom";
import {Reducer, useEffect, useReducer, useState} from "react";
import Header from "../../features/datasets/AddDataset/Header";
import {FunctionComponent, Reducer, useEffect, useReducer, useState} from "react";
import {
MDSActionType,
MDSReducerActionType,
} from '../../features/mitmDatasets/AddMitMDataset/types';
import {datasetReducer} from "../DatasetCreation";
import useDatasetsList from "../../features/datasets/hooks/useDatasetLists";
import {t} from "@superset-ui/core";
import {MitMDatasetObject} from "../../features/mitmDatasets/types";
import withToasts from 'src/components/MessageToasts/withToasts';
// @ts-ignore
const prevUrl =
'/mitm_dataset/list/?pageIndex=0&sortColumn=changed_on_delta_humanized&sortOrder=desc';
......@@ -39,13 +38,19 @@ export function mitmDatasetReducer(
}
}
export default function AddMitMDataset() {
interface AddMitMDatasetProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
}
const AddMitMDataset: FunctionComponent<AddMitMDatasetProps> = ({ addDangerToast,
addSuccessToast }: AddMitMDatasetProps) => {
// @ts-ignore
const [dataset, setDataset] = useReducer<
Reducer<Partial<MitMDatasetObject> | null, MDSReducerActionType>
>(mitmDatasetReducer, null);
const [editPageIsVisible, setEditPageIsVisible] = useState(false);
const { datasetId: id } = useParams<{ datasetId: string }>();
useEffect(() => {
if (!Number.isNaN(parseInt(id, 10))) {
......@@ -59,7 +64,9 @@ export default function AddMitMDataset() {
<h2>AddMitMDatasetPage</h2>
<p>{dataset}</p>
<p>{CREATE_DATASET_TEXT}</p>
{"editpageisvisible: " + editPageIsVisible}
{'editpageisvisible: ' + editPageIsVisible}
</div>
);
}
};
export default withToasts(AddMitMDataset);
......@@ -49,6 +49,7 @@ import {
} from 'src/features/mitmDatasets/constants';
import { ModifiedInfo } from 'src/components/AuditInfo';
import MitMDatasetModal from "../../features/mitmDatasets/MitMDatasetModal";
import UploadMitMDatasetModal from "../../features/mitmDatasets/UploadMitMDatasetModal";
const Actions = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base};
......@@ -115,7 +116,10 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({
const [datasetCurrentlyEditing, setDatasetCurrentlyEditing] =
useState<MitMDataset | null>(null);
const [importingDataset, showImportModal] = useState<boolean>(false);
const [uploadModalOpen, setUploadModalOpen] = useState<boolean>(false);
const [preparingExport, setPreparingExport] = useState<boolean>(false);
const openDatasetImportModal = () => {
......@@ -420,6 +424,20 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({
buttonStyle: 'primary',
});
buttonArr.push({
name: (
<Tooltip
id="upload-tooltip"
title={t('Upload dataset')}
placement="bottomRight"
>
<Icons.Import data-test="upload-button" />
</Tooltip>
),
buttonStyle: 'link',
onClick: () => setUploadModalOpen(true),
})
buttonArr.push({
name: (
<Tooltip
......@@ -486,6 +504,12 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({
return (
<>
<SubMenu {...menuData} />
<UploadMitMDatasetModal
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
show={uploadModalOpen}
onHide={() => setUploadModalOpen(false)}
/>
{datasetCurrentlyDeleting && (
<DeleteModal
description={
......@@ -573,6 +597,7 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({
show={importingDataset}
onHide={closeDatasetImportModal}
/>
{preparingExport && <Loading />}
</>
);
......
......@@ -38,6 +38,7 @@ class ForwardableRequest(pydantic.BaseModel):
if request.content_encoding == 'multipart/form-data':
form_data = request.form.to_dict()
files = {}
td = None
if len(request.files) > 0 and save_files:
td = tempfile.mkdtemp(prefix='forwarded_request')
for n, f in request.files.items():
......
......@@ -20,3 +20,4 @@ class MitMDatasetView(BaseSupersetView):
def list(self) -> FlaskResponse:
return super().render_app_template()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment