diff --git a/CHANGELOG.md b/CHANGELOG.md index 1def570dbd9..adf74a7e05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). - Rendered isosurfaces in the 3D viewport can now be interacted with. Shift+Click on an isosurface will jump exactly to where you clicked. Also, hovering over an isosurface will highlight that cell in all viewports. [#3858](https://github.com/scalableminds/webknossos/pull/3858) - webKnossos now comes with a list of sample datasets that can be automatically downloaded and imported from the menu. [#3725](https://github.com/scalableminds/webknossos/pull/3725) - Added a shortcut (Q) and button in the actions dropdown to screenshot the tracing views. The screenshots will contain everything that is visible in the tracing views, so feel free to disable the crosshairs in the settings or toggle the tree visibility using the (1) and (2) shortcuts before triggering the screenshot. [#3834](https://github.com/scalableminds/webknossos/pull/3834) +- Neuroglancer precomputed datasets can now be added to webKnossos using the webknossos-connect (wk-connect) service. To setup a wk-connect datastore follow the instructions in the [Readme](https://github.com/scalableminds/webknossos-connect). Afterwards, datasets can be added through "Add Dataset" - "Add Dataset via wk-connect". [#3843](https://github.com/scalableminds/webknossos/pull/3843) - The dataset settings within the tracing view allow to select between different loading strategies now ("best quality first" and "progressive quality"). Additionally, the rendering can use different magnifications as a fallback (instead of only one magnification). [#3801](https://github.com/scalableminds/webknossos/pull/3801) - The mapping selection dropbown is now sorted alphabetically. [#3864](https://github.com/scalableminds/webknossos/pull/3864) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index f9f75011b18..93f0d095836 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -8,7 +8,7 @@ User-facing changes are documented in the [changelog](CHANGELOG.md). - To ensure that the existing behavior for loading data is preserved ("best quality first" as opposed to the new "progressive quality" default) execute: `update webknossos.user_datasetconfigurations set configuration = configuration || jsonb '{"loadingStrategy":"BEST_QUALITY_FIRST"}'`. See [#3801](https://github.com/scalableminds/webknossos/pull/3801) for additional context. ### Postgres Evolutions: -- +- [041-add-datastore-isconnector.sql](conf/evolutions/041-add-datastore-isconnector.sql) ## [19.03.0](https://github.com/scalableminds/webknossos/releases/tag/19.03.0) - 2019-03-04 diff --git a/app/controllers/InitialDataController.scala b/app/controllers/InitialDataController.scala index d328d3ca9bc..d835bc670bb 100644 --- a/app/controllers/InitialDataController.scala +++ b/app/controllers/InitialDataController.scala @@ -205,7 +205,7 @@ Samplecountry if (conf.Datastore.enabled) { dataStoreDAO.findOneByName("localhost").futureBox.map { maybeStore => if (maybeStore.isEmpty) { - logger.info("inserting local datastore"); + logger.info("inserting local datastore") dataStoreDAO.insertOne(DataStore("localhost", conf.Http.uri, conf.Datastore.key)) } } @@ -215,7 +215,7 @@ Samplecountry if (conf.Tracingstore.enabled) { tracingStoreDAO.findOneByName("localhost").futureBox.map { maybeStore => if (maybeStore.isEmpty) { - logger.info("inserting local tracingstore"); + logger.info("inserting local tracingstore") tracingStoreDAO.insertOne(TracingStore("localhost", conf.Http.uri, conf.Tracingstore.key)) } } diff --git a/app/models/binary/DataSetService.scala b/app/models/binary/DataSetService.scala index 4306e089cf7..11da1bb859a 100644 --- a/app/models/binary/DataSetService.scala +++ b/app/models/binary/DataSetService.scala @@ -108,7 +108,7 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, .getWithJsonResponse[InboxDataSource] def addForeignDataStore(name: String, url: String)(implicit ctx: DBAccessContext): Fox[Unit] = { - val dataStore = DataStore(name, url, "", isForeign = true) // the key can be "" because keys are only important for own DataStore. Own Datastores have a key that is not "" + val dataStore = DataStore(name, url, "", isForeign = true, isConnector = false) // the key can be "" because keys are only important for own DataStore. Own Datastores have a key that is not "" for { _ <- dataStoreDAO.insertOne(dataStore) } yield () diff --git a/app/models/binary/DataStore.scala b/app/models/binary/DataStore.scala index a0ddb7180c2..9e6f74461e1 100644 --- a/app/models/binary/DataStore.scala +++ b/app/models/binary/DataStore.scala @@ -19,7 +19,8 @@ case class DataStore( key: String, isScratch: Boolean = false, isDeleted: Boolean = false, - isForeign: Boolean = false + isForeign: Boolean = false, + isConnector: Boolean = false ) class DataStoreService @Inject()(dataStoreDAO: DataStoreDAO)(implicit ec: ExecutionContext) @@ -32,7 +33,8 @@ class DataStoreService @Inject()(dataStoreDAO: DataStoreDAO)(implicit ec: Execut "name" -> dataStore.name, "url" -> dataStore.url, "isForeign" -> dataStore.isForeign, - "isScratch" -> dataStore.isScratch + "isScratch" -> dataStore.isScratch, + "isConnector" -> dataStore.isConnector )) def validateAccess[A](name: String)(block: (DataStore) => Future[Result])(implicit request: Request[A], @@ -61,7 +63,8 @@ class DataStoreDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext r.key, r.isscratch, r.isdeleted, - r.isforeign + r.isforeign, + r.isconnector )) def findOneByKey(key: String)(implicit ctx: DBAccessContext): Fox[DataStore] = @@ -95,8 +98,8 @@ class DataStoreDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext def insertOne(d: DataStore): Fox[Unit] = for { - _ <- run(sqlu"""insert into webknossos.dataStores(name, url, key, isScratch, isDeleted, isForeign) - values(${d.name}, ${d.url}, ${d.key}, ${d.isScratch}, ${d.isDeleted}, ${d.isForeign})""") + _ <- run(sqlu"""insert into webknossos.dataStores(name, url, key, isScratch, isDeleted, isForeign, isConnector) + values(${d.name}, ${d.url}, ${d.key}, ${d.isScratch}, ${d.isDeleted}, ${d.isForeign}, ${d.isConnector})""") } yield () } diff --git a/conf/evolutions/041-add-datastore-isconnector.sql b/conf/evolutions/041-add-datastore-isconnector.sql new file mode 100644 index 00000000000..443b5c3747d --- /dev/null +++ b/conf/evolutions/041-add-datastore-isconnector.sql @@ -0,0 +1,13 @@ +-- https://github.com/scalableminds/webknossos/pull/3843 + +START TRANSACTION; + +DROP VIEW webknossos.dataStores_; + +ALTER TABLE webknossos.dataStores ADD COLUMN isConnector BOOLEAN NOT NULL DEFAULT false; + +CREATE VIEW webknossos.dataStores_ AS SELECT * FROM webknossos.dataStores WHERE NOT isDeleted; + +UPDATE webknossos.releaseInformation SET schemaVersion = 41; + +COMMIT TRANSACTION; diff --git a/conf/evolutions/reversions/041-add-datastore-isconnector.sql b/conf/evolutions/reversions/041-add-datastore-isconnector.sql new file mode 100644 index 00000000000..5643b072ce4 --- /dev/null +++ b/conf/evolutions/reversions/041-add-datastore-isconnector.sql @@ -0,0 +1,11 @@ +START TRANSACTION; + +DROP VIEW webknossos.dataStores_; + +ALTER TABLE webknossos.dataStores DROP COLUMN isConnector; + +CREATE VIEW webknossos.dataStores_ AS SELECT * FROM webknossos.dataStores WHERE NOT isDeleted; + +UPDATE webknossos.releaseInformation SET schemaVersion = 40; + +COMMIT TRANSACTION; diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index fc78e6dd4a8..91c1f6c3c0e 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -44,6 +44,7 @@ import { type ServerSkeletonTracing, type ServerTracing, type ServerVolumeTracing, + type WkConnectDatasetConfig, } from "admin/api_flow_types"; import type { DatasetConfiguration } from "oxalis/store"; import type { NewTask, TaskCreationResponse } from "admin/task/task_create_bulk_view"; @@ -770,8 +771,8 @@ export function getDatasetAccessList(datasetId: APIDatasetId): Promise { - await doWithToken(token => +export function addDataset(datasetConfig: DatasetConfig): Promise { + return doWithToken(token => Request.sendMultipartFormReceiveJSON(`/data/datasets?token=${token}`, { data: datasetConfig, host: datasetConfig.datastore, @@ -779,6 +780,19 @@ export async function addDataset(datasetConfig: DatasetConfig): Promise { ); } +export function addWkConnectDataset( + datastoreHost: string, + datasetConfig: WkConnectDatasetConfig, +): Promise { + return doWithToken(token => + Request.sendJSONReceiveJSON(`/data/datasets?token=${token}`, { + data: datasetConfig, + host: datastoreHost, + method: "POST", + }), + ); +} + export async function addForeignDataSet( dataStoreName: string, url: string, diff --git a/frontend/javascripts/admin/api_flow_types.js b/frontend/javascripts/admin/api_flow_types.js index f18c09d7a94..2f7d0f9217e 100644 --- a/frontend/javascripts/admin/api_flow_types.js +++ b/frontend/javascripts/admin/api_flow_types.js @@ -74,8 +74,9 @@ export type APIDataSource = APIDataSourceBase & { export type APIDataStore = { +name: string, +url: string, - +isForeign?: boolean, + +isForeign: boolean, +isScratch: boolean, + +isConnector: boolean, }; export type APITracingStore = { @@ -369,6 +370,22 @@ export type DatasetConfig = { +zipFile: File, }; +type WkConnectLayer = { + // This is the source URL of the layer, should start with gs://, http:// or https:// + source: string, + type: "image" | "segmentation", +}; + +export type WkConnectDatasetConfig = { + neuroglancer: { + [organizationName: string]: { + [datasetName: string]: { + layers: { [layerName: string]: WkConnectLayer }, + }, + }, + }, +}; + export type APITimeTracking = { time: string, timestamp: number, diff --git a/frontend/javascripts/admin/dataset/dataset_add_view.js b/frontend/javascripts/admin/dataset/dataset_add_view.js index 59c49943b16..5323ed979f0 100644 --- a/frontend/javascripts/admin/dataset/dataset_add_view.js +++ b/frontend/javascripts/admin/dataset/dataset_add_view.js @@ -5,14 +5,17 @@ import React from "react"; import { connect } from "react-redux"; import _ from "lodash"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser, APIDataStore } from "admin/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; import DatasetAddForeignView from "admin/dataset/dataset_add_foreign_view"; +import DatasetAddWkConnectView from "admin/dataset/dataset_add_wk_connect_view"; import DatasetUploadView from "admin/dataset/dataset_upload_view"; import SampleDatasetsModal from "dashboard/dataset/sample_datasets_modal"; import features from "features"; +import { getDatastores } from "admin/admin_rest_api"; import renderIndependently from "libs/render_independently"; +import { useFetch } from "libs/react_helpers"; const { TabPane } = Tabs; @@ -34,49 +37,80 @@ const renderSampleDatasetsModal = (user: APIUser, history: RouterHistory) => { )); }; -const DatasetAddView = ({ history, activeUser }: PropsWithRouter) => ( - - - - - Upload Dataset - - } - key="1" - > - { - const url = `/datasets/${organization}/${datasetName}/import`; - history.push(url); - }} - /> - - {features().addForeignDataset ? ( +const fetchCategorizedDatastores = async (): Promise<{ + own: Array, + wkConnect: Array, +}> => { + const fetchedDatastores = await getDatastores(); + return { + own: fetchedDatastores.filter(ds => !ds.isForeign && !ds.isConnector), + wkConnect: fetchedDatastores.filter(ds => ds.isConnector), + }; +}; + +const DatasetAddView = ({ history, activeUser }: PropsWithRouter) => { + const datastores = useFetch(fetchCategorizedDatastores, { own: [], wkConnect: [] }, []); + + const handleDatasetAdded = (organization: string, datasetName: string) => { + const url = `/datasets/${organization}/${datasetName}/import`; + history.push(url); + }; + + return ( + + - - Add foreign Dataset + + Upload Dataset } - key="2" + key="1" > - history.push("/dashboard")} /> + - ) : null} - - - -); + {datastores.wkConnect.length > 0 && ( + + + Add Dataset via wk-connect + + } + key="2" + > + + + )} + {features().addForeignDataset && ( + + + Add Foreign Dataset + + } + key="3" + > + history.push("/dashboard")} /> + + )} + + + + ); +}; const mapStateToProps = (state: OxalisState) => ({ activeUser: enforceActiveUser(state.activeUser), diff --git a/frontend/javascripts/admin/dataset/dataset_add_wk_connect_view.js b/frontend/javascripts/admin/dataset/dataset_add_wk_connect_view.js new file mode 100644 index 00000000000..fe3fec67aa1 --- /dev/null +++ b/frontend/javascripts/admin/dataset/dataset_add_wk_connect_view.js @@ -0,0 +1,144 @@ +// @flow +import { Form, Input, Button, Col, Row } from "antd"; +import { connect } from "react-redux"; +import React from "react"; +import _ from "lodash"; + +import type { APIDataStore, APIUser } from "admin/api_flow_types"; +import type { OxalisState } from "oxalis/store"; +import { addWkConnectDataset } from "admin/admin_rest_api"; +import messages from "messages"; +import Toast from "libs/toast"; +import * as Utils from "libs/utils"; +import { trackAction } from "oxalis/model/helpers/analytics"; +import { + CardContainer, + DatasetNameFormItem, + DatastoreFormItem, +} from "admin/dataset/dataset_components"; + +const FormItem = Form.Item; + +type OwnProps = {| + datastores: Array, + withoutCard?: boolean, + onAdded: (string, string) => void, +|}; +type StateProps = {| + activeUser: ?APIUser, +|}; +type Props = {| ...OwnProps, ...StateProps |}; +type PropsWithForm = {| + ...Props, + form: Object, +|}; + +class DatasetAddWkConnectView extends React.PureComponent { + validateAndParseUrl(url: string) { + const delimiterIndex = url.indexOf("#!"); + if (delimiterIndex < 0) { + throw new Error("The URL doesn't contain the #! delimiter. Please insert the full URL."); + } + + const jsonConfig = url.slice(delimiterIndex + 2); + // This will throw an error if the URL did not contain valid JSON. The error will be handled by the caller. + const config = JSON.parse(decodeURIComponent(jsonConfig)); + config.layers.forEach(layer => { + if (!layer.source.startsWith("precomputed://")) { + throw new Error( + "This dataset contains layers that are not supported by wk-connect. wk-connect supports only 'precomputed://' neuroglancer layers.", + ); + } + }); + return config; + } + + handleSubmit = evt => { + evt.preventDefault(); + const { activeUser } = this.props; + + this.props.form.validateFields(async (err, formValues) => { + if (err || activeUser == null) return; + + const neuroglancerConfig = this.validateAndParseUrl(formValues.url); + const fullLayers = _.keyBy(neuroglancerConfig.layers, "name"); + // Remove unnecessary attributes of the layer, the precomputed source prefix needs to be removed as well + const layers = _.mapValues(fullLayers, ({ source, type }) => ({ + type, + source: source.replace(/^(precomputed:\/\/)/, ""), + })); + + const datasetConfig = { + neuroglancer: { + [activeUser.organization]: { + [formValues.name]: { + layers, + }, + }, + }, + }; + + trackAction("Add wk-connect dataset"); + await addWkConnectDataset(formValues.datastore, datasetConfig); + + Toast.success(messages["dataset.add_success"]); + await Utils.sleep(3000); // wait for 3 seconds so the server can catch up / do its thing + this.props.onAdded(activeUser.organization, formValues.name); + }); + }; + + render() { + const { form, activeUser, withoutCard, datastores } = this.props; + const { getFieldDecorator } = form; + + return ( +
+ + Currently wk-connect supports adding Neuroglancer datasets. Simply set a dataset name, + select the wk-connect datastore and paste the URL to the Neuroglancer dataset. +
+ + + + + + + + + + {getFieldDecorator("url", { + rules: [ + { required: true, message: messages["dataset.import.required.url"] }, + { + validator: async (_rule, value, callback) => { + try { + this.validateAndParseUrl(value); + callback(); + } catch (error) { + callback(error); + } + }, + }, + ], + validateFirst: true, + })()} + + + + +
+
+
+ ); + } +} + +const mapStateToProps = (state: OxalisState): StateProps => ({ + activeUser: state.activeUser, +}); + +export default connect(mapStateToProps)( + Form.create()(DatasetAddWkConnectView), +); diff --git a/frontend/javascripts/admin/dataset/dataset_components.js b/frontend/javascripts/admin/dataset/dataset_components.js new file mode 100644 index 00000000000..3f923d1a174 --- /dev/null +++ b/frontend/javascripts/admin/dataset/dataset_components.js @@ -0,0 +1,95 @@ +// @flow +import * as React from "react"; + +import { Form, Input, Select, Card } from "antd"; +import messages from "messages"; +import { isDatasetNameValid } from "admin/admin_rest_api"; +import type { APIDataStore, APIUser } from "admin/api_flow_types"; + +const FormItem = Form.Item; +const { Option } = Select; + +export function CardContainer({ + children, + withoutCard, + title, +}: { + children: React.Node, + withoutCard: ?boolean, + title: string, +}) { + if (withoutCard) { + return {children}; + } else { + return ( + {title}} + > + {children} + + ); + } +} + +export function DatasetNameFormItem({ form, activeUser }: { form: Object, activeUser: ?APIUser }) { + const { getFieldDecorator } = form; + return ( + + {getFieldDecorator("name", { + rules: [ + { required: true, message: messages["dataset.import.required.name"] }, + { min: 3 }, + { pattern: /[0-9a-zA-Z_-]+$/ }, + { + validator: async (_rule, value, callback) => { + if (!activeUser) throw new Error("Can't do operation if no user is logged in."); + const reasons = await isDatasetNameValid({ + name: value, + owningOrganization: activeUser.organization, + }); + if (reasons != null) { + callback(reasons); + } else { + callback(); + } + }, + }, + ], + validateFirst: true, + })()} + + ); +} + +export function DatastoreFormItem({ + form, + datastores, +}: { + form: Object, + datastores: Array, +}) { + const { getFieldDecorator } = form; + return ( + + {getFieldDecorator("datastore", { + rules: [{ required: true, message: messages["dataset.import.required.datastore"] }], + initialValue: datastores.length ? datastores[0].url : null, + })( + , + )} + + ); +} diff --git a/frontend/javascripts/admin/dataset/dataset_upload_view.js b/frontend/javascripts/admin/dataset/dataset_upload_view.js index b960508e7d3..116b1d877d1 100644 --- a/frontend/javascripts/admin/dataset/dataset_upload_view.js +++ b/frontend/javascripts/admin/dataset/dataset_upload_view.js @@ -1,20 +1,25 @@ // @flow -import { Form, Input, Select, Button, Card, Spin, Upload, Icon, Col, Row } from "antd"; +import { Form, Button, Spin, Upload, Icon, Col, Row } from "antd"; import { connect } from "react-redux"; import React from "react"; import type { APIDataStore, APIUser, DatasetConfig } from "admin/api_flow_types"; import type { OxalisState } from "oxalis/store"; -import { getDatastores, addDataset, isDatasetNameValid } from "admin/admin_rest_api"; +import { addDataset } from "admin/admin_rest_api"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; import messages from "messages"; import { trackAction } from "oxalis/model/helpers/analytics"; +import { + CardContainer, + DatasetNameFormItem, + DatastoreFormItem, +} from "admin/dataset/dataset_components"; const FormItem = Form.Item; -const { Option } = Select; type OwnProps = {| + datastores: Array, withoutCard?: boolean, onUploaded: (string, string) => void, |}; @@ -28,32 +33,14 @@ type PropsWithForm = {| |}; type State = { - datastores: Array, isUploading: boolean, }; class DatasetUploadView extends React.PureComponent { state = { - datastores: [], isUploading: false, }; - componentDidMount() { - this.fetchData(); - } - - async fetchData() { - const datastores = await getDatastores(); - - this.setState({ - datastores, - }); - - if (datastores.length > 0) { - this.props.form.setFieldsValue({ datastore: datastores[0].url }); - } - } - normFile = e => { if (Array.isArray(e)) { return e; @@ -99,77 +86,20 @@ class DatasetUploadView extends React.PureComponent { }; render() { - const { getFieldDecorator } = this.props.form; - - const Container = ({ children }) => { - if (this.props.withoutCard) { - return {children}; - } else { - return ( - Upload Dataset} - > - {children} - - ); - } - }; + const { form, activeUser, withoutCard, datastores } = this.props; + const { getFieldDecorator } = form; + return (
- +
- - {getFieldDecorator("name", { - rules: [ - { required: true, message: messages["dataset.import.required.name"] }, - { min: 3 }, - { pattern: /[0-9a-zA-Z_-]+$/ }, - { - validator: async (_rule, value, callback) => { - if (!this.props.activeUser) - throw new Error("Can't do operation if no user is logged in."); - const reasons = await isDatasetNameValid({ - name: value, - owningOrganization: this.props.activeUser.organization, - }); - if (reasons != null) { - callback(reasons); - } else { - callback(); - } - }, - }, - ], - validateFirst: true, - })()} - + - - {getFieldDecorator("datastore", { - rules: [ - { required: true, message: messages["dataset.import.required.datastore"] }, - ], - })( - , - )} - + @@ -181,7 +111,7 @@ class DatasetUploadView extends React.PureComponent { { - this.props.form.setFieldsValue({ zipFile: [file] }); + form.setFieldsValue({ zipFile: [file] }); return false; }} > @@ -201,7 +131,7 @@ class DatasetUploadView extends React.PureComponent {
-
+
); diff --git a/frontend/javascripts/admin/onboarding.js b/frontend/javascripts/admin/onboarding.js index a64db1d8a1d..0bb99129a77 100644 --- a/frontend/javascripts/admin/onboarding.js +++ b/frontend/javascripts/admin/onboarding.js @@ -18,7 +18,7 @@ import Clipboard from "clipboard-js"; import React, { type Node, useState, useEffect } from "react"; import { Link } from "react-router-dom"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser, APIDataStore } from "admin/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { location } from "libs/window"; import DatasetImportView from "dashboard/dataset/dataset_import_view"; @@ -27,7 +27,7 @@ import RegistrationForm from "admin/auth/registration_form"; import Toast from "libs/toast"; import renderIndependently from "libs/render_independently"; import SampleDatasetsModal from "dashboard/dataset/sample_datasets_modal"; -import { getOrganizations } from "admin/admin_rest_api"; +import { getOrganizations, getDatastores } from "admin/admin_rest_api"; const { Step } = Steps; const FormItem = Form.Item; @@ -39,6 +39,7 @@ type Props = StateProps; type State = { currentStep: number, + datastores: Array, organizationName: string, datasetNameToImport: ?string, showDatasetUploadModal: boolean, @@ -244,11 +245,21 @@ const OrganizationForm = Form.create()(({ form, onComplete }) => { class OnboardingView extends React.PureComponent { state = { currentStep: 0, + datastores: [], organizationName: "", showDatasetUploadModal: false, datasetNameToImport: null, }; + componentDidMount() { + this.fetchData(); + } + + async fetchData() { + const datastores = (await getDatastores()).filter(ds => !ds.isForeign && !ds.isConnector); + this.setState({ datastores }); + } + advanceStep = () => { this.setState(prevState => ({ currentStep: prevState.currentStep + 1, @@ -336,6 +347,7 @@ class OnboardingView extends React.PureComponent { onCancel={() => this.setState({ showDatasetUploadModal: false })} > { this.setState({ datasetNameToImport: datasetName, showDatasetUploadModal: false }); }} diff --git a/frontend/javascripts/dashboard/dataset/dataset_import_view.js b/frontend/javascripts/dashboard/dataset/dataset_import_view.js index b12262d2068..a9f9d6a32b0 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_import_view.js +++ b/frontend/javascripts/dashboard/dataset/dataset_import_view.js @@ -235,7 +235,11 @@ class DatasetImportView extends React.PureComponent { await updateDatasetTeams(dataset, teamIds); const dataSource = JSON.parse(formValues.dataSourceJson); - if (this.state.dataset != null && !this.state.dataset.isForeign) { + if ( + this.state.dataset != null && + !this.state.dataset.isForeign && + !this.state.dataset.dataStore.isConnector + ) { await updateDatasetDatasource(this.props.datasetId.name, dataset.dataStore.url, dataSource); } @@ -389,7 +393,10 @@ class DatasetImportView extends React.PureComponent {