Skip to content

Commit

Permalink
Merge branch 'release_24.0' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Apr 17, 2024
2 parents bc248ef + 33538be commit 67436a6
Show file tree
Hide file tree
Showing 21 changed files with 184 additions and 67 deletions.
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@galaxyproject/galaxy-client",
"version": "23.1.1",
"version": "24.0.0",
"description": "Galaxy client application build system",
"keywords": [
"galaxy"
Expand Down
9 changes: 8 additions & 1 deletion client/src/components/Form/Elements/FormData/FormData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { orList } from "@/utils/strings";
import type { DataOption } from "./types";
import { BATCH, SOURCE, VARIANTS } from "./variants";
import FormSelection from "../FormSelection.vue";
import FormSelect from "@/components/Form/Elements/FormSelect.vue";
library.add(faCopy, faFile, faFolder, faCaretDown, faCaretUp, faExclamation, faLink, faUnlink);
Expand Down Expand Up @@ -502,7 +503,7 @@ const noOptionsWarningMessage = computed(() => {
</div>

<FormSelect
v-if="currentVariant"
v-if="currentVariant && !currentVariant.multiple"
v-model="currentValue"
class="align-self-start"
:multiple="currentVariant.multiple"
Expand All @@ -515,6 +516,12 @@ const noOptionsWarningMessage = computed(() => {
</BAlert>
</template>
</FormSelect>
<FormSelection
v-else-if="currentVariant?.multiple"
v-model="currentValue"
:data="formattedOptions"
optional
multiple />

<template v-if="currentVariant && currentVariant.batch !== BATCH.DISABLED">
<BFormCheckbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ const searchRegex = computed(() => {
/** Wraps value prop so it can be set, and always returns an array */
const selected = computed({
get() {
return Array.isArray(props.value) ? props.value : [props.value];
if (props.value === null) {
return [];
} else if (Array.isArray(props.value)) {
return props.value;
} else {
return [props.value];
}
},
set(value) {
emit("input", value);
Expand Down Expand Up @@ -142,7 +148,15 @@ async function deselectOption(event: MouseEvent, index: number) {
const [option] = selectedOptionsFiltered.value.splice(index, 1);
if (option) {
const i = selected.value.indexOf(option.value);
const i = selected.value.findIndex((selectedValue) => {
if (typeof selectedValue === "string") {
return selectedValue === option.value;
} else if (typeof selectedValue === "object" && typeof option.value === "object") {
// in case values are objects, compare their ids (if they have the 'id' property)
return selectedValue?.id === option.value?.id;
}
return false;
});
selected.value = selected.value.flatMap((value, index) => (index === i ? [] : [value]));
}
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/Form/Elements/FormSelection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ watch(
}
if (newValue === "none") {
if ((Array.isArray(props.value) && props.value.length >= 15) || props.options.length >= 500) {
if (
(Array.isArray(props.value) && props.value.length >= 15) ||
(props.options && props.options.length >= 500)
) {
useMany.value = true;
} else {
useMany.value = false;
Expand Down
10 changes: 7 additions & 3 deletions client/src/components/History/HistoryView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
<b-button
v-if="userOwnsHistory"
size="sm"
variant="outline-info"
:title="setAsCurrentTitle"
:disabled="isSetAsCurrentDisabled"
data-description="switch to history button"
Expand All @@ -20,13 +19,14 @@
v-if="canImportHistory"
v-b-modal:copy-history-modal
size="sm"
variant="outline-info"
title="Import this history"
data-description="import history button">
Import this history
</b-button>
</div>

<b-alert :show="copySuccess"> History imported and set to your active history. </b-alert>

<CollectionPanel
v-if="selectedCollections.length && selectedCollections[0].history_id == id"
:history="history"
Expand All @@ -40,7 +40,7 @@
filterable
@view-collection="onViewCollection" />

<CopyModal id="copy-history-modal" :history="history" />
<CopyModal id="copy-history-modal" :history="history" @ok="copyOkay" />
</div>
</template>

Expand Down Expand Up @@ -69,6 +69,7 @@ export default {
data() {
return {
selectedCollections: [],
copySuccess: false,
};
},
computed: {
Expand Down Expand Up @@ -129,6 +130,9 @@ export default {
onViewCollection(collection) {
this.selectedCollections = [...this.selectedCollections, collection];
},
copyOkay() {
this.copySuccess = true;
},
},
};
</script>
31 changes: 31 additions & 0 deletions client/src/components/Markdown/MarkdownToolBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,35 @@ export default {
this.getVisualizations();
},
methods: {
getSteps() {
const steps = [];
this.steps &&
Object.values(this.steps).forEach((step) => {
if (step.label) {
steps.push(step.label);
}
});
return steps;
},
getOutputs(filterByType = undefined) {
const outputLabels = [];
this.steps &&
Object.values(this.steps).forEach((step) => {
step.workflow_outputs.forEach((workflowOutput) => {
if (workflowOutput.label) {
if (!filterByType || this.stepOutputMatchesType(step, workflowOutput, filterByType)) {
outputLabels.push(workflowOutput.label);
}
}
});
});
return outputLabels;
},
stepOutputMatchesType(step, workflowOutput, type) {
return Boolean(
step.outputs.find((output) => output.name === workflowOutput.output_name && output.type === type)
);
},
getArgumentTitle(argumentName) {
return (
argumentName[0].toUpperCase() +
Expand Down Expand Up @@ -311,11 +340,13 @@ export default {
onHistoryDatasetId(argumentName) {
this.selectedArgumentName = argumentName;
this.selectedType = "history_dataset_id";
this.selectedLabels = this.getOutputs("data");
this.selectedShow = true;
},
onHistoryCollectionId(argumentName) {
this.selectedArgumentName = argumentName;
this.selectedType = "history_dataset_collection_id";
this.selectedLabels = this.getOutputs("collection");
this.selectedShow = true;
},
onWorkflowId(argumentName) {
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/plugins/localization.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ function localizeDirective(l) {
// TODO consider using a different hook if we need dynamic updates in content translation
bind(el, binding, vnode) {
el.childNodes.forEach((node) => {
// trim for lookup, but put back whitespace after
const leadingSpace = node.textContent.match(/^\s*/)[0];
const trailingSpace = node.textContent.match(/\s*$/)[0];
const standardizedContent = node.textContent
.replace(newlineMatch, " ")
.replace(doublespaces, " ")
.trim();
node.textContent = l(standardizedContent);
node.textContent = leadingSpace + l(standardizedContent) + trailingSpace;
});
},
};
Expand Down
5 changes: 2 additions & 3 deletions lib/galaxy/authnz/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,8 @@ def refresh_expiring_oidc_tokens_for_provider(self, trans, auth):
if refreshed:
log.debug(f"Refreshed user token via `{auth.provider}` identity provider")
return True
except Exception as e:
msg = f"An error occurred when refreshing user token: {e}"
log.error(msg)
except Exception:
log.exception("An error occurred when refreshing user token")
return False

def refresh_expiring_oidc_tokens(self, trans, user=None):
Expand Down
10 changes: 7 additions & 3 deletions lib/galaxy/config/sample/file_sources_conf.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,14 @@
doc: Make sure to define this generic drs file source if you have defined any other drs file sources, or stock drs download capability will be disabled.

- type: inveniordm
id: invenio
doc: Invenio RDM turn-key research data management repository
label: Invenio RDM Demo Repository
id: invenio_sandbox
doc: This is the Sandbox instance of Invenio. It is used for testing purposes only, content is NOT preserved. DOIs created in this instance are not real and will not resolve.
label: Invenio RDM Sandbox Repository (TESTING ONLY)
url: https://inveniordm.web.cern.ch/
token: ${user.user_vault.read_secret('preferences/invenio_sandbox/token')}
# token: ${user.preferences['invenio_sandbox|token']} # Alternatively use this for retrieving the token from user preferences instead of the Vault
public_name: ${user.preferences['invenio_sandbox|public_name']}
writable: true

- type: onedata
id: onedata1
Expand Down
41 changes: 18 additions & 23 deletions lib/galaxy/files/sources/_rdm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
from typing import (
cast,
List,
NamedTuple,
Optional,
Expand All @@ -26,6 +25,7 @@
class RDMFilesSourceProperties(FilesSourceProperties):
url: str
token: str
public_name: str


class RecordFilename(NamedTuple):
Expand Down Expand Up @@ -80,7 +80,9 @@ def get_files_in_record(
"""
raise NotImplementedError()

def create_draft_record(self, title: str, user_context: OptionalUserContext = None):
def create_draft_record(
self, title: str, public_name: Optional[str] = None, user_context: OptionalUserContext = None
):
"""Creates a draft record (directory) in the repository with basic metadata.
The metadata is usually just the title of the record and the user that created it.
Expand Down Expand Up @@ -138,22 +140,17 @@ class RDMFilesSource(BaseFilesSource):

def __init__(self, **kwd: Unpack[FilesSourceProperties]):
props = self._parse_common_config_opts(kwd)
base_url = props.get("url", None)
base_url = props.get("url")
if not base_url:
raise Exception("URL for RDM repository must be provided in configuration")
self._repository_url = base_url
self._token = props.get("token", None)
self._props = props
self._repository_interactor = self.get_repository_interactor(base_url)

@property
def repository(self) -> RDMRepositoryInteractor:
return self._repository_interactor

@property
def token(self) -> Optional[str]:
return self._token if self._token and not self._token.startswith("$") else None

def get_repository_interactor(self, repository_url: str) -> RDMRepositoryInteractor:
"""Returns an interactor compatible with the given repository URL.
Expand Down Expand Up @@ -190,25 +187,23 @@ def get_error_msg(details: str) -> str:
def get_record_id_from_path(self, source_path: str) -> str:
return self.parse_path(source_path, record_id_only=True).record_id

def _serialization_props(self, user_context: OptionalUserContext = None) -> RDMFilesSourceProperties:
def _serialization_props(self, user_context: OptionalUserContext = None):
effective_props = {}
for key, val in self._props.items():
effective_props[key] = self._evaluate_prop(val, user_context=user_context)
effective_props["url"] = self._repository_url
effective_props["token"] = self.safe_get_authorization_token(user_context)
return cast(RDMFilesSourceProperties, effective_props)
return effective_props

def get_authorization_token(self, user_context: OptionalUserContext) -> str:
token = self.token
if not token and user_context:
vault = user_context.user_vault if user_context else None
token = vault.read_secret(f"preferences/{self.id}/token") if vault else None
if token is None:
raise AuthenticationRequired(f"No authorization token provided in user's settings for '{self.label}'")
token = None
if user_context:
effective_props = self._serialization_props(user_context)
token = effective_props.get("token")
if not token:
raise AuthenticationRequired(
f"Please provide a personal access token in your user's preferences for '{self.label}'"
)
return token

def safe_get_authorization_token(self, user_context: OptionalUserContext) -> Optional[str]:
try:
return self.get_authorization_token(user_context)
except AuthenticationRequired:
return None
def get_public_name(self, user_context: OptionalUserContext) -> Optional[str]:
effective_props = self._serialization_props(user_context)
return effective_props.get("public_name")
30 changes: 18 additions & 12 deletions lib/galaxy/files/sources/invenio.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ def _create_entry(
user_context: OptionalUserContext = None,
opts: Optional[FilesSourceOptions] = None,
) -> Entry:
record = self.repository.create_draft_record(entry_data["name"], user_context=user_context)
public_name = self.get_public_name(user_context)
record = self.repository.create_draft_record(entry_data["name"], public_name, user_context=user_context)
return {
"uri": self.repository.to_plugin_uri(record["id"]),
"name": record["metadata"]["title"],
Expand Down Expand Up @@ -198,9 +199,11 @@ def get_files_in_record(
response_data = self._get_response(user_context, request_url)
return self._get_record_files_from_response(record_id, response_data)

def create_draft_record(self, title: str, user_context: OptionalUserContext = None) -> RemoteDirectory:
def create_draft_record(
self, title: str, public_name: Optional[str] = None, user_context: OptionalUserContext = None
) -> RemoteDirectory:
today = datetime.date.today().isoformat()
creator = self._get_creator_from_user_context(user_context)
creator = self._get_creator_from_public_name(public_name)
create_record_request = {
"files": {"enabled": True},
"metadata": {
Expand Down Expand Up @@ -360,23 +363,26 @@ def _get_record_files_from_response(self, record_id: str, response: dict) -> Lis
)
return rval

def _get_creator_from_user_context(self, user_context: OptionalUserContext):
public_name = self.get_user_preference_by_key("public_name", user_context)
family_name = "Galaxy User"
def _get_creator_from_public_name(self, public_name: Optional[str] = None) -> Creator:
given_name = "Anonymous"
family_name = "Galaxy User"
if public_name:
tokens = public_name.split(", ")
if len(tokens) == 2:
family_name = tokens[0]
given_name = tokens[1]
else:
given_name = public_name
return {"person_or_org": {"family_name": family_name, "given_name": given_name, "type": "personal"}}

def get_user_preference_by_key(self, key: str, user_context: OptionalUserContext):
preferences = user_context.preferences if user_context else None
value = preferences.get(f"{self.plugin.id}|{key}", None) if preferences else None
return value
return {
"person_or_org": {
"name": f"{given_name} {family_name}",
"family_name": family_name,
"given_name": given_name,
"type": "personal",
"identifiers": [],
},
"affiliations": [],
}

def _get_response(
self, user_context: OptionalUserContext, request_url: str, params: Optional[Dict[str, Any]] = None
Expand Down
Loading

0 comments on commit 67436a6

Please sign in to comment.