Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from "react"
import { gql, useMutation } from "@apollo/client"
import PropTypes from "prop-types"
import { Button } from "../../components/button/Button"
import styled from "@emotion/styled"

const ButtonRow = styled.div`
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-gap: 1.3em;
place-items: start;
`

const SuccessMessage = styled.p({
color: "rgb(92, 184, 92)",
})
const InProgressMessage = styled.p({
color: "orange",
})
const ErrorMessage = styled.p({
color: "red",
})

const CREATE_MOSAIC = gql`
mutation createMosaic($datasetId: ID!, $ref: String!) {
createMosaic(datasetId: $datasetId, ref: $ref)
}
`

const CreateMosaic = ({ datasetId, revision }) => {
const [mosaicMutation, { data, loading, error }] = useMutation(
CREATE_MOSAIC,
)
const success = data && data.createMosaic
return (
<div className="dataset-form">
{loading && (
<InProgressMessage>Mosaic creation is starting.</InProgressMessage>
)}
{error && <ErrorMessage>An error has occurred.</ErrorMessage>}
{success && <SuccessMessage>Mosaic creation has begun.</SuccessMessage>}
<ButtonRow>
<Button
icon="fa fa-rocket"
label="Create Mosaic"
primary={true}
size="small"
onClick={() => {
mosaicMutation({
variables: {
datasetId,
ref: revision,
},
})
}}
/>
</ButtonRow>
</div>
)
}

CreateMosaic.propTypes = {
datasetId: PropTypes.string,
revision: PropTypes.string,
}

export default CreateMosaic
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DatasetHistory from "../fragments/dataset-history.jsx"
import CacheClear from "../mutations/cache-clear.jsx"
import { HoldDeletion } from "../mutations/hold-deletion"
import AdminExports from "../mutations/admin-exports"
import CreateMosaic from "../mutations/create-mosaic"
import { DatasetPageBorder } from "./styles/dataset-page-border"
import { HeaderRow3, HeaderRow4 } from "./styles/header-row"
import { DatasetEvents } from "../mutations/dataset-events"
Expand Down Expand Up @@ -38,6 +39,15 @@ const AdminDataset = ({ dataset }) => (
<AdminExports />
</div>
<hr />
<HeaderRow4>Create Mosaic PDF</HeaderRow4>
<p>
Create a pdf containing a single slice of every anatomical image. Useful
for quickly finding non-defaced images.
</p>
<div className="dataset-form-controls">
<CreateMosaic datasetId={dataset.id} revision={dataset.draft.head} />
</div>
<hr />
<HeaderRow4>Draft Head</HeaderRow4>
{dataset.draft.head}
<hr />
Expand Down
87 changes: 87 additions & 0 deletions packages/openneuro-server/src/graphql/resolvers/mosaic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import config from "../../config"
import { generateDataladCookie } from "../../libs/authentication/jwt"
import { getDatasetWorker } from "../../libs/datalad-service"
import Mosaic from "../../models/mosaic"
import { checkDatasetWrite } from "../permissions"
import type { GraphQLContext } from "../builder"

export const mosaic = (dataset, _, _context: GraphQLContext) =>
Mosaic.findOne({
id: dataset.revision,
datasetId: dataset.id,
})
.exec()
.then(Boolean)

/**
* Snapshot mosaic resolver
*/
export const snapshotMosaic = async (snapshot, _, context: GraphQLContext) => {
const dataset = {
id: snapshot.id.split(":")[0],
revision: snapshot.hexsha,
}
return mosaic(dataset, _, context)
}

/**
* Save mosaic data returned by the datalad service
*
* Returns only a boolean if successful or not
*/
export const updateMosaic = (obj, args) => {
return Mosaic.updateOne(
{
id: args.mosaic.id,
datasetId: args.mosaic.datasetId,
},
args.mosaic,
{
upsert: true,
},
)
.exec()
.then(() => true)
}

export const mosaicUrl = (datasetId, ref) => {
return `http://${
getDatasetWorker(
datasetId,
)
}/datasets/${datasetId}/mosaic/${ref}`
}

/**
* Request mosaic creation for a given commit
* @param {object} obj Parent resolver
* @param {object} args
* @param {string} args.datasetId Dataset accession number
* @param {string} args.ref Git hexsha
*/
export const createMosaic = async (
obj,
{ datasetId, ref },
{ user, userInfo }: Pick<GraphQLContext, "user" | "userInfo">,
) => {
await checkDatasetWrite(datasetId, user, userInfo)
try {
const response = await fetch(mosaicUrl(datasetId, ref), {
method: "POST",
body: JSON.stringify({}),
headers: {
cookie: generateDataladCookie(config)(userInfo),
},
})
if (response.status === 200) {
return true
} else {
return false
}
} catch (err) {
// Backend unavailable or lock failed
if (err) {
return false
}
}
}
3 changes: 3 additions & 0 deletions packages/openneuro-server/src/graphql/resolvers/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { removeUser, setAdmin, setBlocked, updateUser } from "./user.js"
import { updateSummary } from "./summary"
import { revalidate, updateValidation } from "./validation.js"
import { createMosaic, updateMosaic } from "./mosaic.js"
import {
removePermissions,
updateOrcidPermissions,
Expand Down Expand Up @@ -70,6 +71,7 @@ const Mutation = {
deleteSnapshot,
updateSummary,
updateValidation,
updateMosaic,
updatePublic,
updatePermissions,
updateOrcidPermissions,
Expand All @@ -93,6 +95,7 @@ const Mutation = {
finishUpload,
cacheClear,
revalidate,
createMosaic,
fsckDataset,
holdDeletion,
prepareRepoAccess,
Expand Down
2 changes: 2 additions & 0 deletions packages/openneuro-server/src/graphql/resolvers/snapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getRedis } from "../../libs/redis"
import CacheItem, { CacheType } from "../../cache/item"
import { normalizeDOI } from "../../libs/doi/normalize"
import { snapshotValidation } from "./validation"
import { snapshotMosaic } from "./mosaic"
import { advancedDatasetSearchConnection } from "./dataset-search"
import { contributors } from "../../datalad/contributors"
import { getDraftInfo } from "../../datalad/draft"
Expand Down Expand Up @@ -334,6 +335,7 @@ const Snapshot = {
issues: (snapshot) => snapshotIssues(snapshot),
issuesStatus: (snapshot) => issuesSnapshotStatus(snapshot),
validation: snapshotValidation,
mosaic: snapshotMosaic,
contributors: (snapshot) => {
const datasetId = snapshot.datasetId
return contributors({
Expand Down
7 changes: 7 additions & 0 deletions packages/openneuro-server/src/graphql/schema/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export const ValidatorInput = builder.inputType("ValidatorInput", {
}),
})

export const MosaicInput = builder.inputType("MosaicInput", {
fields: (t) => ({
id: t.id({ required: true }),
datasetId: t.id({ required: true }),
}),
})

export const MetadataInput = builder.inputType("MetadataInput", {
fields: (t) => ({
datasetId: t.id({ required: true }),
Expand Down
15 changes: 15 additions & 0 deletions packages/openneuro-server/src/graphql/schema/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
MetadataInput,
SummaryInput,
ValidatorInput,
MosaicInput
} from "./inputs"

import Mutation from "../resolvers/mutation"
Expand Down Expand Up @@ -122,6 +123,12 @@ builder.mutationType({
},
resolve: (root, args) => Mutation.updateValidation(root, args as never),
}),
updateMosaic: t.boolean({
args: {
mosaic: t.arg({ type: MosaicInput, required: true }),
},
resolve: (root, args) => Mutation.updateMosaic(root, args as never),
}),
updatePermissions: t.field({
type: DatasetPermissions,
args: {
Expand Down Expand Up @@ -332,6 +339,14 @@ builder.mutationType({
resolve: (root, args, ctx) =>
Mutation.revalidate(root, args as never, ctx),
}),
createMosaic: t.boolean({
args: {
datasetId: t.arg.id({ required: true }),
ref: t.arg.string({ required: true }),
},
resolve: (root, args, ctx) =>
Mutation.createMosaic(root, args as never, ctx),
}),
prepareRepoAccess: t.field({
type: RepoMetadata,
args: {
Expand Down
4 changes: 4 additions & 0 deletions packages/openneuro-server/src/graphql/schema/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,9 @@ SnapshotRef.implement({
nullable: { list: true, items: true },
resolve: (obj) => SnapshotFields.contributors(obj),
}),
mosaic: t.boolean({
resolve: (obj, _args, ctx) =>
SnapshotFields.mosaic(obj, null, ctx) as never,
}),
}),
})
18 changes: 18 additions & 0 deletions packages/openneuro-server/src/handlers/datalad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import mime from "mime-types"
import { getFiles } from "../datalad/files"
import { getDatasetEndpoint, getDatasetWorker } from "../libs/datalad-service"
import { getDraftRevision } from "../datalad/draft"
import { checkDatasetRead } from "../graphql/permissions"

/**
* Handlers for datalad dataset manipulation
Expand Down Expand Up @@ -120,3 +121,20 @@ export function gitRepo(req, res) {
)
return res.redirect(301, newUrl)
}

/**
* Request a mosaic pdf for a dataset commit
*/
export const getMosaic = async (req, res) => {
const { datasetId, ref } = req.params
try {
await checkDatasetRead(datasetId, req.user?.id, req.user)
} catch {
res.status(403).send("You do not have access to read this dataset.")
return
}
const worker = getDatasetWorker(datasetId)
res.set("Content-Type", "application/pdf")
const uri = `${worker}/datasets/${datasetId}/mosaic/${ref}`
return request.get(uri).pipe(res)
}
21 changes: 21 additions & 0 deletions packages/openneuro-server/src/models/mosaic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import mongoose from "mongoose"
import type { Document } from "mongoose"
const { Schema, model } = mongoose


/**
* Mosaic output collection
*/
export interface MosaicDocument extends Document {
id: string
datasetId: string
}

const mosaicSchema = new Schema({
id: { type: String, required: true },
datasetId: { type: String, required: true },
})

const Mosaic = model<MosaicDocument>("Mosaic", mosaicSchema)

export default Mosaic
6 changes: 6 additions & 0 deletions packages/openneuro-server/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ const routes = [
url: "/datasets/:datasetId/objects/:key",
handler: datalad.getObject,
},
{
method: "get",
url: "/datasets/:datasetId/mosaic/:ref",
middleware: [noCache, jwt.authenticate],
handler: datalad.getMosaic,
},

// Authentication routes

Expand Down
3 changes: 3 additions & 0 deletions services/datalad/datalad_service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from datalad_service.handlers.upload import UploadResource
from datalad_service.handlers.upload_file import UploadFileResource
from datalad_service.handlers.validation import ValidationResource
from datalad_service.handlers.mosaic import MosaicResource
from datalad_service.handlers.git import (
GitRefsResource,
GitReceiveResource,
Expand Down Expand Up @@ -123,6 +124,7 @@ def create_app():
datasets = DatasetResource(store)
dataset_draft = DraftResource(store)
dataset_validation = ValidationResource(store)
dataset_mosaic = MosaicResource(store)
dataset_history = HistoryResource(store)
dataset_drop = DropResource(store)
dataset_description = DescriptionResource(store)
Expand Down Expand Up @@ -155,6 +157,7 @@ def create_app():
app.add_route('/datasets/{dataset:dataset}/drop', dataset_drop)
app.add_route('/datasets/{dataset:dataset}/description', dataset_description)
app.add_route('/datasets/{dataset:dataset}/validate/{hexsha}', dataset_validation)
app.add_route('/datasets/{dataset:dataset}/mosaic/{hexsha}', dataset_mosaic)
app.add_route('/datasets/{dataset:dataset}/reset/{hexsha}', dataset_reset_resource)
app.add_route('/datasets/{dataset:dataset}/fsck', dataset_fsck_resource)
app.add_route('/datasets/{dataset:dataset}/info', dataset_info_resource)
Expand Down
Loading