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

Metadata view #32

Merged
merged 27 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions src/lib/components/ContentContainer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import DropZone from "$lib/components/DropZone.svelte";
import UrlInput from "$lib/components/UrlInput.svelte";
import Provenance from "$lib/components/Provenance.svelte";
import Metadata from "$lib/components/Metadata.svelte";
import About from "$lib/components/About.svelte";
import Error from "$lib/components/Error.svelte";
import Loading from "$lib/components/Loading.svelte";
import provenanceModel from "$lib/models/provenanceModel";

let loadingComponent: undefined | HTMLElement;
</script>
Expand Down Expand Up @@ -82,6 +84,13 @@
>
<Provenance />
</div>
<div
class={$url.pathname.replaceAll("/", "") === "metadata" && $loading.status !== "LOADING"
? "tab"
: "hidden-tab"}
>
<Metadata />
</div>
{/if}
</div>
</div>
Expand Down
76 changes: 76 additions & 0 deletions src/lib/components/Metadata.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script lang="ts">
import "../../app.css";

import provenanceModel from "$lib/models/provenanceModel";
import { getFile } from "$lib/scripts/fileutils";
import Panel from "./Panel.svelte";
import readerModel from "$lib/models/readerModel";
</script>

<Panel header="Warning: Do not include confidential information in your metadata.">
<p>
QIIME 2 goes to great lengths to ensure that your bioinformatics workflow
will be reproducible. This includes recording information about your analysis
inside of your Results’ data provenance, and the recorded information includes
metadata that you provided to run specific commands. For this and other reasons,
we strongly recommend that you never include confidential information, such
as Personally Identifying Information (PII), in your QIIME 2 metadata. Because
QIIME 2 stores metadata in your data provenance, confidential information that
you use in a QIIME 2 analysis will persist in downstream Results.
</p>
<br>
<p>
Instead of including confidential information in your metadata, you should
encode it with variables that only authorized individuals have access to.
For example, subject names should be replaced with anonymized subject identifiers
before use with QIIME 2.
</p>
</Panel>
<table class="w-full">
<tr class="border-b border-gray-400 text-gray-800">
<th class="text-left p-1">Plugin</th>
<th class="text-left p-1">Action</th>
<th class="text-left p-1">Execution UUID</th>
<th class="text-left p-1">Filename</th>
<th class="text-left p-1">Download</th>
</tr>
{#each $provenanceModel.metadata as [plugin, action, executionUUID, metadataFile, artifactUUID]}
<tr class="border-t border-gray-300 text-gray-600">
<td class="p-1 py-2">{plugin}</td>
<td class="p-1">{action}</td>
<td class="p-1">{executionUUID}</td>
<td class="p-1">{metadataFile}</td>
<td class="p-1 text-blue-700">
<button on:click={async () => {
let metadataFilePath;

if (artifactUUID === provenanceModel.uuid) {
metadataFilePath = `provenance/action/${metadataFile}`;
} else {
metadataFilePath = `provenance/artifacts/${artifactUUID}/action/${metadataFile}`
}

const file = await getFile(
metadataFilePath,
readerModel.uuid,
provenanceModel.zipReader).then(
(data) => new Blob(
[data.byteArray],
{ type: data.type }
)
)
const link = document.createElement('a');

link.href = URL.createObjectURL(file);
link.download = metadataFile;
link.click();
URL.revokeObjectURL(link.href);
}
}
class="hover:text-gray-600">
Download
</button>
</td>
</tr>
{/each}
</table>
4 changes: 2 additions & 2 deletions src/lib/components/NavBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
{/if}
<!-- If the screen is wide enough slap the buttons here in a grid -->
<ul class="hidden lg:grid grid-flow-col gap-6 items-center">
<NavButtons {readerModel} />
<NavButtons/>
</ul>
<!-- If it isn't wide enough make them collapsible -->
{#if $readerModel.rawSrc}
Expand Down Expand Up @@ -190,7 +190,7 @@
<div id="nav-dropdown">
{#if $openCollapsible}
<ul use:melt={$content} transition:slide class="lg:hidden">
<NavButtons {readerModel} />
<NavButtons />
</ul>
{/if}
</div>
Expand Down
13 changes: 11 additions & 2 deletions src/lib/components/NavButtons.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import readerModel from "$lib/models/readerModel";
import provenanceModel from "$lib/models/provenanceModel";
import url from "$lib/scripts/url-store";

export let readerModel;
</script>

{#if $readerModel.indexPath}
Expand Down Expand Up @@ -34,4 +34,13 @@
Provenance
</button>
</li>
<li>
<button
class={$url.pathname.replaceAll("/", "") === "metadata" ? "selected-nav-button nav-button" : "nav-button"}
on:click={() => (history.pushState({}, "", "/metadata/"+window.location.search))}
title='Metadata'
>
Metadata
</button>
</li>
{/if}
57 changes: 57 additions & 0 deletions src/lib/models/provenanceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ import JSZip from "jszip";

import BiMap from "$lib/scripts/biMap";
import { getYAML } from "$lib/scripts/fileutils";
import { currentMetadataStore } from "$lib/scripts/currentMetadataStore";

const ACTION_TYPES_WITH_HISTORY = ["method", "visualizer", "pipeline"];

let currentMetadata: Set<string>;

currentMetadataStore.subscribe((value) => {
currentMetadata = value.currentMetadata;
});

/**
* This class is a subscribable svelte store that manages parsing and storing provenance
* information for the provided Result.
Expand Down Expand Up @@ -39,6 +46,9 @@ class ProvenanceModel {
nodeIDToJSON = new BiMap();
keys: Set<string> = new Set();

seenMetadata: Set<string> = new Set();
metadata: Array<Array<string>> = [];

// Class attributes passed in by readerModel pertaining to currently loaded
// Result
uuid: string = "";
Expand Down Expand Up @@ -94,6 +104,9 @@ class ProvenanceModel {
this.nodeIDToJSON.clear();
this.keys = new Set();

this.seenMetadata = new Set();
this.metadata = [];

this.uuid = uuid;
this.zipReader = zipReader;

Expand Down Expand Up @@ -127,6 +140,14 @@ class ProvenanceModel {
const sourceAction = await this.getProvenanceAction(resultUUID);
const sourceActionUUID = sourceAction.execution.uuid;

// Make this "-" to match q2-<plugin>
if (sourceAction.action.action !== undefined) {
sourceAction.action.action = sourceAction.action.action.replaceAll(
"_",
"-",
);
}

// If this Result is in a Collection, we need to set this to
// paramName:destinationActionUUID:sourceActionUUID in place of resultUUID
// as our unique identifier in some places
Expand Down Expand Up @@ -159,6 +180,10 @@ class ProvenanceModel {
return this.heightMap.get(sourceActionUUID);
}

// If we get here we haven't seen this result yet so we need to track any
// metadata it might have
this._handleMetadata(sourceAction, resultUUID);

// If we have already seen this Action then short circuit
if (await this._handleAction(resultID, sourceActionUUID, sourceAction)) {
return this.heightMap.get(sourceActionUUID);
Expand Down Expand Up @@ -322,6 +347,38 @@ class ProvenanceModel {
return false;
}

/**
* Checks if we parsed any metadata from this artifact and tracks it if we
* did
*
* @param {object} sourceAction - The Action that received this metadata as
* input.
*/
_handleMetadata(sourceAction: object, resultUUID: string) {
if (currentMetadata.length !== 0) {
for (const metadataFile of currentMetadata) {
const identifier = `${sourceAction.execution.uuid} ${metadataFile}`;

if (!this.seenMetadata.has(identifier)) {
this.seenMetadata.add(identifier);

this.metadata.push([
sourceAction.action.plugin,
sourceAction.action.action,
sourceAction.execution.uuid,
metadataFile,
resultUUID,
]);
}
}

// Set this back to empty for the next artifact that has metadata
currentMetadataStore.set({
currentMetadata: new Set(),
});
}
}

/**
* Iterate over and recurse up from all Results that were provided as QIIME 2
* inputs to the Action that produced the Result we are currently parsing.
Expand Down
7 changes: 7 additions & 0 deletions src/lib/scripts/currentMetadataStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { writable } from "svelte/store";

export const currentMetadataStore = writable<{
currentMetadata: Set<string>;
}>({
currentMetadata: new Set(),
});
32 changes: 29 additions & 3 deletions src/lib/scripts/yaml-schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import yaml from "js-yaml";
import { currentMetadataStore } from "./currentMetadataStore";

let currentMetadata: Set<string>;

currentMetadataStore.subscribe((value) => {
currentMetadata = value.currentMetadata;
});

export default yaml.Schema.create([
new yaml.Type("!no-provenance", {
Expand All @@ -11,17 +18,36 @@ export default yaml.Schema.create([
new yaml.Type("!ref", {
kind: "scalar",
resolve: (data) => data !== null,
construct: (data) => data,
construct: (data) => {
// Data will be of form environment:plugins:<plugin>
const plugin = data.split(":")[2];
return `q2-${plugin}`;
},
}),
new yaml.Type("!metadata", {
kind: "scalar",
resolve: (data) => data !== null,
construct: (data) => {
const splitData = data.split(":");
let constructed;

if (splitData.length === 1) {
return { file: data, artifacts: [] };
currentMetadata.add(splitData[0]);
constructed = { file: data, artifacts: [] };
} else {
currentMetadata.add(splitData[1]);
constructed = {
file: splitData[1],
artifacts: splitData[0].split(","),
};
}
return { file: splitData[1], artifacts: splitData[0].split(",") };

// Update the store
currentMetadataStore.set({
currentMetadata: currentMetadata,
});

return constructed;
},
}),
new yaml.Type("!color", {
Expand Down
Empty file.