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

Enable/Disabled worker jobs in frontend #7591

Merged
merged 16 commits into from
Mar 4, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Email verification is disabled by default. To enable it, set `webKnossos.user.emailVerification.activated` to `true` in your `application.conf`. [#7620](https://github.com/scalableminds/webknossos/pull/7620) [#7621](https://github.com/scalableminds/webknossos/pull/7621)
- Added more documentation for N5 and Neuroglancer precomputed web upload. [#7622](https://github.com/scalableminds/webknossos/pull/7622)
- Added the config key `webKnossos.user.timeTrackingOnlyWithSignificantChanges`, which when set to `true` will only track time if the user has made significant changes to the annotation. [#7627](https://github.com/scalableminds/webknossos/pull/7627)
- Only display UI elements to launch background jobs if the (worker) backend actually supports them. [#7591](https://github.com/scalableminds/webknossos/pull/7591)

### Fixed
- Fixed rare SIGBUS crashes of the datastore module that were caused by memory mapping on unstable file systems. [#7528](https://github.com/scalableminds/webknossos/pull/7528)
Expand Down
17 changes: 12 additions & 5 deletions app/models/dataset/DataStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models.dataset
import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext}
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.schema.Tables._
import models.job.JobService

import javax.inject.Inject
import play.api.i18n.{Messages, MessagesProvider}
Expand All @@ -11,7 +12,7 @@ import play.api.mvc.{Result, Results}
import slick.jdbc.PostgresProfile.api._
import slick.lifted.Rep
import utils.sql.{SQLDAO, SqlClient, SqlToken}
import utils.ObjectId
import utils.{ObjectId, WkConf}

import scala.concurrent.{ExecutionContext, Future}

Expand Down Expand Up @@ -56,18 +57,24 @@ object DataStore {
fromForm(name, url, publicUrl, "", isScratch, allowsUpload)
}

class DataStoreService @Inject()(dataStoreDAO: DataStoreDAO)(implicit ec: ExecutionContext)
class DataStoreService @Inject()(dataStoreDAO: DataStoreDAO, jobService: JobService, conf: WkConf)(
implicit ec: ExecutionContext)
extends FoxImplicits
with Results {

def publicWrites(dataStore: DataStore): Fox[JsObject] =
Fox.successful(
for {
jobsSupportedByAvailableWorkers <- jobService.jobsSupportedByAvailableWorkers(dataStore.name)
jobsEnabled = conf.Features.jobsEnabled && jobsSupportedByAvailableWorkers.nonEmpty
} yield
Json.obj(
"name" -> dataStore.name,
"url" -> dataStore.publicUrl,
"isScratch" -> dataStore.isScratch,
"allowsUpload" -> dataStore.allowsUpload
))
"allowsUpload" -> dataStore.allowsUpload,
"jobsSupportedByAvailableWorkers" -> Json.toJson(jobsSupportedByAvailableWorkers),
"jobsEnabled" -> jobsEnabled
)

def validateAccess(name: String, key: String)(block: DataStore => Future[Result])(
implicit m: MessagesProvider): Fox[Result] =
Expand Down
4 changes: 0 additions & 4 deletions app/models/dataset/DatasetService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,6 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
lastUsedByUser <- lastUsedTimeFor(dataset._id, requestingUserOpt) ?~> "dataset.list.fetchLastUsedTimeFailed"
dataStoreJs <- dataStoreService.publicWrites(dataStore) ?~> "dataset.list.dataStoreWritesFailed"
dataSource <- dataSourceFor(dataset, Some(organization)) ?~> "dataset.list.fetchDataSourceFailed"
jobsSupportedByAvailableWorkers <- jobService.jobsSupportedByAvailableWorkers(dataStore.name)
jobsEnabled = conf.Features.jobsEnabled && jobsSupportedByAvailableWorkers.nonEmpty
} yield {
Json.obj(
"name" -> dataset.name,
Expand All @@ -354,8 +352,6 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
"sortingKey" -> dataset.sortingKey,
"details" -> dataset.details,
"isUnreported" -> Json.toJson(isUnreported(dataset)),
"jobsEnabled" -> jobsEnabled,
"jobsSupportedByAvailableWorkers" -> Json.toJson(jobsSupportedByAvailableWorkers),
"tags" -> dataset.tags,
"folderId" -> dataset._folder,
// included temporarily for compatibility with webknossos-libs, until a better versioning mechanism is implemented
Expand Down
54 changes: 37 additions & 17 deletions frontend/javascripts/admin/dataset/dataset_upload_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import { useDropzone, FileWithPath } from "react-dropzone";
import ErrorHandling from "libs/error_handling";
import { Link, RouteComponentProps } from "react-router-dom";
import { withRouter } from "react-router-dom";
import type {
APITeam,
APIDataStore,
APIUser,
APIDatasetId,
APIOrganization,
import {
type APITeam,
type APIDataStore,
type APIUser,
type APIDatasetId,
type APIOrganization,
APIJobType,
} from "types/api_flow_types";
import type { OxalisState } from "oxalis/store";
import {
Expand Down Expand Up @@ -178,6 +179,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
currentFormRef.setFieldsValue({
datastoreUrl: uploadableDatastores[0].url,
});
this.setState({ datastoreUrl: uploadableDatastores[0].url });
}
}
}
Expand Down Expand Up @@ -554,26 +556,22 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
needsConversion,
});

if (needsConversion && !features().jobsEnabled) {
if (needsConversion && !this.isDatasetConversionEnabled()) {
form.setFieldsValue({
zipFile: [],
});
Modal.info({
content: (
<div>
The selected dataset does not seem to be in the WKW or Zarr format. Please convert the
dataset using{" "}
<a
target="_blank"
href="https://github.com/scalableminds/webknossos-libs/tree/master/wkcuber#webknossos-cuber-wkcuber"
rel="noopener noreferrer"
>
webknossos-cuber
dataset using the{" "}
<a target="_blank" href="https://docs.webknossos.org/cli" rel="noopener noreferrer">
webknossos CLI
</a>
, the{" "}
<a
target="_blank"
href="https://github.com/scalableminds/webknossos-libs/tree/master/webknossos#webknossos-python-library"
href="https://docs.webknossos.org/webknossos-py"
rel="noopener noreferrer"
>
webknossos Python library
Expand Down Expand Up @@ -606,6 +604,22 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
}
};

isDatasetConversionEnabled = () => {
const uploadableDatastores = this.props.datastores.filter(
(datastore) => datastore.allowsUpload,
);
const hasOnlyOneDatastoreOrNone = uploadableDatastores.length <= 1;

const selectedDatastore = hasOnlyOneDatastoreOrNone
? uploadableDatastores[0]
: this.getDatastoreForUrl(this.state.datastoreUrl);
philippotto marked this conversation as resolved.
Show resolved Hide resolved

return (
selectedDatastore?.jobsSupportedByAvailableWorkers.includes(APIJobType.CONVERT_TO_WKW) ||
false
);
};

render() {
const form = this.formRef.current;
const { activeUser, withoutCard, datastores } = this.props;
Expand All @@ -614,6 +628,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
const { needsConversion } = this.state;
const uploadableDatastores = datastores.filter((datastore) => datastore.allowsUpload);
const hasOnlyOneDatastoreOrNone = uploadableDatastores.length <= 1;

return (
<div
className="dataset-administration"
Expand Down Expand Up @@ -641,6 +656,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {

<Form
onFinish={this.handleSubmit}
onValuesChange={(_, { datastoreUrl }) => this.setState({ datastoreUrl })}
layout="vertical"
ref={this.formRef}
initialValues={{
Expand Down Expand Up @@ -702,7 +718,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
datastores={uploadableDatastores}
hidden={hasOnlyOneDatastoreOrNone}
/>
{features().jobsEnabled && needsConversion ? (
{this.isDatasetConversionEnabled() && needsConversion ? (
<FormItemWithInfo
name="scale"
label="Voxel Size"
Expand Down Expand Up @@ -829,6 +845,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
this.validateFiles(files);
}}
fileList={[]}
isDatasetConversionEnabled={this.isDatasetConversionEnabled()}
/>
</FormItem>
<FormItem
Expand Down Expand Up @@ -860,9 +877,11 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
function FileUploadArea({
fileList,
onChange,
isDatasetConversionEnabled,
}: {
fileList: FileWithPath[];
onChange: (files: FileWithPath[]) => void;
isDatasetConversionEnabled: boolean;
}) {
const onDropAccepted = (acceptedFiles: FileWithPath[]) => {
// file.path should be set by react-dropzone (which uses file-selector::toFileWithPath).
Expand All @@ -881,6 +900,7 @@ function FileUploadArea({
<li key={file.path}>{file.path}</li>
));
const showSmallFileList = files.length > 10;

const list = (
<List
itemLayout="horizontal"
Expand Down Expand Up @@ -946,7 +966,7 @@ function FileUploadArea({
>
Drag your file(s) to this area to upload them. Either add individual image files, a zip
archive or a folder.{" "}
{features().jobsEnabled ? (
{isDatasetConversionEnabled ? (
<>
<br />
<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
Space,
Button,
} from "antd";
import features from "features";
import * as React from "react";
import { Vector3Input, BoundingBoxInput } from "libs/vector_input";
import { getBitDepth } from "oxalis/model/accessors/dataset_accessor";
Expand All @@ -30,7 +29,7 @@ import { DataLayer } from "types/schemas/datasource.types";
import { getDatasetNameRules, layerNameRules } from "admin/dataset/dataset_components";
import { useSelector } from "react-redux";
import { DeleteOutlined } from "@ant-design/icons";
import { APIDataLayer, APIDatasetId } from "types/api_flow_types";
import { APIDataLayer, APIDataset, APIJobType } from "types/api_flow_types";
import { useStartAndPollJob } from "admin/job/job_hooks";
import { Vector3 } from "oxalis/constants";
import Toast from "libs/toast";
Expand Down Expand Up @@ -66,14 +65,14 @@ export default function DatasetSettingsDataTab({
activeDataSourceEditMode,
onChange,
additionalAlert,
datasetId,
dataset,
}: {
allowRenamingDataset: boolean;
form: FormInstance;
activeDataSourceEditMode: "simple" | "advanced";
onChange: (arg0: "simple" | "advanced") => void;
additionalAlert?: React.ReactNode | null | undefined;
datasetId?: APIDatasetId | undefined;
dataset?: APIDataset | null | undefined;
}) {
// Using the return value of useWatch for the `dataSource` var
// yields outdated values. Therefore, the hook only exists for listening.
Expand Down Expand Up @@ -119,7 +118,7 @@ export default function DatasetSettingsDataTab({
<Hideable hidden={activeDataSourceEditMode !== "simple"}>
<RetryingErrorBoundary>
<SimpleDatasetForm
datasetId={datasetId}
dataset={dataset}
allowRenamingDataset={allowRenamingDataset}
form={form}
dataSource={dataSource}
Expand Down Expand Up @@ -153,12 +152,12 @@ function SimpleDatasetForm({
allowRenamingDataset,
dataSource,
form,
datasetId,
dataset,
}: {
allowRenamingDataset: boolean;
dataSource: Record<string, any>;
form: FormInstance;
datasetId?: APIDatasetId | undefined;
dataset: APIDataset | null | undefined;
}) {
const activeUser = useSelector((state: OxalisState) => state.activeUser);
const onRemoveLayer = (layer: DataLayer) => {
Expand Down Expand Up @@ -257,7 +256,7 @@ function SimpleDatasetForm({
// the layer name may change in this view, the order does not, so idx is the right key choice here
<List.Item key={`layer-${idx}`}>
<SimpleLayerForm
datasetId={datasetId}
dataset={dataset}
layer={layer}
index={idx}
onRemoveLayer={onRemoveLayer}
Expand All @@ -283,13 +282,13 @@ function SimpleLayerForm({
index,
onRemoveLayer,
form,
datasetId,
dataset,
}: {
layer: DataLayer;
index: number;
onRemoveLayer: (layer: DataLayer) => void;
form: FormInstance;
datasetId?: APIDatasetId | undefined;
dataset: APIDataset | null | undefined;
}) {
const dataLayers = Form.useWatch(["dataSource", "dataLayers"]);
const category = Form.useWatch(["dataSource", "dataLayers", index, "category"]);
Expand Down Expand Up @@ -318,18 +317,18 @@ function SimpleLayerForm({
);
},
initialJobKeyExtractor: (job) =>
job.type === "find_largest_segment_id" && job.datasetName === datasetId?.name
job.type === "find_largest_segment_id" && job.datasetName === dataset?.name
? job.datasetName ?? "largest_segment_id"
: null,
});
const activeJob = runningJobs[0];

const startJobFn =
datasetId != null
dataset != null
? async () => {
const job = await startFindLargestSegmentIdJob(
datasetId.name,
datasetId.owningOrganization,
dataset.name,
dataset.owningOrganization,
layer.name,
);
Toast.info(
Expand Down Expand Up @@ -561,7 +560,9 @@ function SimpleLayerForm({
return parseInt(value, 10);
}}
/>
{datasetId && features().jobsEnabled && (
{dataset?.dataStore.jobsSupportedByAvailableWorkers.includes(
APIJobType.FIND_LARGEST_SEGMENT_ID,
) ? (
<Button
type={mostRecentSuccessfulJob == null ? "primary" : "default"}
title={`${
Expand All @@ -578,6 +579,8 @@ function SimpleLayerForm({
>
Detect
</Button>
) : (
<></>
)}
</DelegatePropsToFirstChild>
</FormItemWithInfo>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ class DatasetSettingsView extends React.PureComponent<PropsWithFormAndRouter, St
{form && (
<DatasetSettingsDataTab
key="SimpleAdvancedDataForm"
datasetId={this.props.datasetId}
dataset={this.state.dataset}
allowRenamingDataset={false}
form={form}
activeDataSourceEditMode={this.state.activeDataSourceEditMode}
Expand Down
3 changes: 2 additions & 1 deletion frontend/javascripts/oxalis/default_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ const defaultState: OxalisState = {
url: "http://localhost:9000",
isScratch: false,
allowsUpload: true,
jobsEnabled: false,
jobsSupportedByAvailableWorkers: [],
},
owningOrganization: "Connectomics department",
description: null,
Expand All @@ -143,7 +145,6 @@ const defaultState: OxalisState = {
allowedTeamsCumulative: [],
logoUrl: null,
lastUsedByUser: 0,
jobsEnabled: false,
sortingKey: 123,
publication: null,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
RenderAnimationOptions,
MOVIE_RESOLUTIONS,
APIDataLayer,
APIJobType,
} from "types/api_flow_types";
import { InfoCircleOutlined } from "@ant-design/icons";
import { PricingEnforcedSpan } from "components/pricing_enforcers";
Expand Down Expand Up @@ -237,14 +238,20 @@ function CreateAnimationModal(props: Props) {
onClose(evt);
};

const isFeatureDisabled = !(
dataset.dataStore.jobsEnabled &&
dataset.dataStore.jobsSupportedByAvailableWorkers.includes(APIJobType.RENDER_ANIMATION)
);

return (
<Modal
title="Create Animation"
open={isOpen}
width={700}
onOk={submitJob}
onCancel={onClose}
okText="Start Animation"
okText={isFeatureDisabled ? "This feature is not available" : "Start Animation"}
okButtonProps={{ disabled: isFeatureDisabled }}
>
<React.Fragment>
<Row gutter={8}>
Expand Down
Loading