Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Clone DataDocs into other Environments #1419

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
11 changes: 9 additions & 2 deletions querybook/server/datasources/datadoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,20 @@ def delete_data_cell_from_doc(doc_id, cell_id):


@register("/datadoc/<int:id>/clone/", methods=["POST"])
def clone_data_doc(id):
def clone_data_doc(id, environment_id=None):
with DBSession() as session:
assert_can_read(id, session=session)

if environment_id is not None:
verify_environment_permission([environment_id])

try:
verify_data_doc_permission(id, session=session)
data_doc = logic.clone_data_doc(
id=id, owner_uid=current_user.id, session=session
id=id,
owner_uid=current_user.id,
environment_id=environment_id,
session=session,
)
doc_dict = data_doc.to_dict(with_cells=True)
except AssertionError as e:
Expand Down
7 changes: 5 additions & 2 deletions querybook/server/logic/datadoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,17 @@ def delete_data_doc(session=None):

# You cannot delete data doc
@with_session
def clone_data_doc(id, owner_uid, commit=True, session=None):
def clone_data_doc(id, owner_uid, environment_id=None, commit=True, session=None):
data_doc = get_data_doc_by_id(id, session=session)

# Check to see if author has permission
assert data_doc is not None, "Invalid data doc id"

# Clone into the same environment if not specified
environment_id = environment_id or data_doc.environment_id

new_data_doc = create_data_doc(
environment_id=data_doc.environment_id,
environment_id=environment_id,
public=data_doc.public,
archived=False,
owner_uid=owner_uid,
Expand Down
38 changes: 0 additions & 38 deletions querybook/webapp/components/DataDoc/DataDoc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,40 +456,6 @@ class DataDocComponent extends React.PureComponent<IProps, IState> {
}));
}

@bind
public onCloneButtonClick() {
const {
cloneDataDoc,
environment,
dataDoc: { id },
} = this.props;
sendConfirm({
header: 'Clone DataDoc?',
message:
'You will be redirected to the new Data Doc after cloning.',
onConfirm: () => {
trackClick({
component: ComponentType.DATADOC_PAGE,
element: ElementType.CLONE_DATADOC_BUTTON,
});
toast.promise(
cloneDataDoc(id).then((dataDoc) =>
history.push(
`/${environment.name}/datadoc/${dataDoc.id}/`
)
),
{
loading: 'Cloning DataDoc...',
success: 'Clone Success!',
error: 'Cloning failed.',
}
);
},
cancelColor: 'default',
confirmIcon: 'Copy',
});
}

@bind
public onQuerycellSelectExecution(cellId: number, executionId: number) {
this.setState(
Expand Down Expand Up @@ -772,7 +738,6 @@ class DataDocComponent extends React.PureComponent<IProps, IState> {
<DataDocRightSidebar
dataDoc={dataDoc}
changeDataDocMeta={changeDataDocMeta}
onClone={this.onCloneButtonClick}
isSaving={isSavingDataDoc}
isEditable={isEditable}
isConnected={connected}
Expand Down Expand Up @@ -939,9 +904,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
changeDataDocMeta: (docId: number, meta: IDataDocMeta) =>
dispatch(dataDocActions.updateDataDocField(docId, 'meta', meta)),

cloneDataDoc: (docId: number) =>
dispatch(dataDocActions.cloneDataDoc(docId)),

insertDataDocCell: (
docId: number,
index: number,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useCallback, useRef } from 'react';
import toast from 'react-hot-toast';
import { useDispatch, useSelector } from 'react-redux';

import { ComponentType, ElementType } from 'const/analytics';
import { trackClick } from 'lib/analytics';
import { sendConfirm } from 'lib/querybookUI';
import history from 'lib/router-history';
import * as dataDocActions from 'redux/dataDoc/action';
import { currentEnvironmentSelector } from 'redux/environment/selector';
import { IEnvironment } from 'redux/environment/types';
import { Dispatch } from 'redux/store/types';
import { IconButton } from 'ui/Button/IconButton';

import { DataDocCloneButtonConfirm } from './DataDocCloneButtonConfirm';

interface IProps {
docId: number;
}

export const DataDocCloneButton: React.FunctionComponent<IProps> = ({
docId,
}) => {
const dispatch: Dispatch = useDispatch();
const currentEnvironment = useSelector(currentEnvironmentSelector);
const selectedEnvironment = useRef<IEnvironment>(currentEnvironment);

const onClone = useCallback(() => {
sendConfirm({
header: 'Clone DataDoc?',
message: (
<DataDocCloneButtonConfirm
defaultEnvironment={currentEnvironment}
onEnvironmentChange={(environment) => {
selectedEnvironment.current = environment;
}}
/>
),
onConfirm: () => {
trackClick({
component: ComponentType.DATADOC_PAGE,
element: ElementType.CLONE_DATADOC_BUTTON,
});
toast.promise(
dispatch(
dataDocActions.cloneDataDoc(
docId,
selectedEnvironment.current.id
)
).then((dataDoc) =>
history.push(
`/${selectedEnvironment.current.name}/datadoc/${dataDoc.id}/`
)
),
{
loading: 'Cloning DataDoc...',
success: 'Clone Success!',
error: 'Cloning failed.',
}
);
},
cancelColor: 'default',
confirmIcon: 'Copy',
});
}, [currentEnvironment, dispatch, docId]);

return (
<IconButton
icon="Copy"
onClick={onClone}
tooltip={'Clone'}
tooltipPos={'left'}
title="Clone"
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';

import { availableEnvironmentsSelector } from 'redux/environment/selector';
import { IEnvironment } from 'redux/environment/types';
import { makeSelectOptions, Select } from 'ui/Select/Select';
import { StyledText } from 'ui/StyledText/StyledText';

interface IProps {
defaultEnvironment: IEnvironment;
onEnvironmentChange: (environment: IEnvironment) => void;
}

export const DataDocCloneButtonConfirm: React.FunctionComponent<IProps> = ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component name seems a bit weird to me. I think this component could be just an environment selector, and move the messages to the DataDocCloneButton component.

defaultEnvironment,
onEnvironmentChange,
}) => {
const availableEnvironments = useSelector(availableEnvironmentsSelector);

const [selectedEnvironmentId, setSelectedEnvironmentId] =
React.useState<number>(defaultEnvironment.id);

const internalEnvironmentChange = useCallback(
(event) => {
if (event.target.value) {
const environmentId = Number(event.target.value);
onEnvironmentChange(
availableEnvironments.find(
(env) => env.id === environmentId
)
);
setSelectedEnvironmentId(environmentId);
}
},
[availableEnvironments, onEnvironmentChange]
);

const selectOptionsDOM = React.useMemo(
() =>
makeSelectOptions(
availableEnvironments.map((env) => ({
value: env.name,
key: env.id,
}))
),
[availableEnvironments]
);

return (
<>
<StyledText className="mb16">
Select the environment to clone the DataDoc to:
</StyledText>
<Select
value={selectedEnvironmentId}
onChange={internalEnvironmentChange}
className="mb16"
>
{selectOptionsDOM}
</Select>
<StyledText>
You will be redirected to the new Data Doc after cloning.
</StyledText>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { fetchDAGExporters } from 'redux/dataDoc/action';
import { IStoreState } from 'redux/store/types';
import { IconButton } from 'ui/Button/IconButton';

import { DataDocCloneButton } from './DataDocCloneButton';
import { DataDocRunAllButton } from './DataDocRunAllButton';
import { DataDocScheduleButton } from './DataDocScheduleButton';
import { DeleteDataDocButton } from './DeleteDataDocButton';
Expand All @@ -27,14 +28,12 @@ interface IProps {
isConnected: boolean;

changeDataDocMeta: (docId: number, meta: IDataDocMeta) => Promise<void>;
onClone: () => any;

onCollapse: () => any;
defaultCollapse: boolean;
}

export const DataDocRightSidebar: React.FunctionComponent<IProps> = ({
onClone,
changeDataDocMeta,

isSaving,
Expand Down Expand Up @@ -83,6 +82,8 @@ export const DataDocRightSidebar: React.FunctionComponent<IProps> = ({
<DataDocRunAllButton docId={dataDoc.id} />
);

const cloneButtonDOM = <DataDocCloneButton docId={dataDoc.id} />;

const buttonSection = (
<div className="DataDocRightSidebar-button-section vertical-space-between">
<div className="DataDocRightSidebar-button-section-top flex-column">
Expand Down Expand Up @@ -137,13 +138,7 @@ export const DataDocRightSidebar: React.FunctionComponent<IProps> = ({
{boardsButtonDOM}
{templateButtonDOM}
{scheduleButtonDOM}
<IconButton
icon="Copy"
onClick={onClone}
tooltip={'Clone'}
tooltipPos={'left'}
title="Clone"
/>
{cloneButtonDOM}
{deleteButtonDOM}
</div>
</div>
Expand Down
10 changes: 8 additions & 2 deletions querybook/webapp/redux/dataDoc/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,15 @@ export function fetchDataDocIfNeeded(docId: number): ThunkResult<Promise<any>> {
};
}

export function cloneDataDoc(docId: number): ThunkResult<Promise<IRawDataDoc>> {
export function cloneDataDoc(
docId: number,
environmentId: number
): ThunkResult<Promise<IRawDataDoc>> {
return async (dispatch) => {
const { data: rawDataDoc } = await DataDocResource.clone(docId);
const { data: rawDataDoc } = await DataDocResource.clone(
docId,
environmentId
);
const { dataDoc, dataDocCellById } = normalizeRawDataDoc(rawDataDoc);

dispatch(receiveDataDoc(dataDoc, dataDocCellById));
Expand Down
5 changes: 4 additions & 1 deletion querybook/webapp/resource/dataDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export const DataDocResource = {
}),
get: (docId: number) => ds.fetch<IRawDataDoc>(`/datadoc/${docId}/`),

clone: (docId: number) => ds.save<IRawDataDoc>(`/datadoc/${docId}/clone/`),
clone: (docId: number, environmentId: number) =>
ds.save<IRawDataDoc>(`/datadoc/${docId}/clone/`, {
environment_id: environmentId,
}),
updateOwner: (docId: number, newOwnerId: number) =>
ds.save<IDataDocEditor>(`/datadoc/${docId}/owner/`, {
next_owner_id: newOwnerId,
Expand Down
Loading