mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #7665 from Nerdinacan/authnz
Authnz UI and associated api
This commit is contained in:
+10
-1
@@ -1,7 +1,11 @@
|
||||
module.exports = {
|
||||
extends: ["eslint:recommended", "plugin:vue/essential"], // airbnb-base, eventually
|
||||
env: {
|
||||
browser: true
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true
|
||||
},
|
||||
parserOptions: { parser: "babel-eslint" },
|
||||
plugins: ["html"],
|
||||
@@ -16,5 +20,10 @@ module.exports = {
|
||||
// properties: "always"
|
||||
// }
|
||||
// ]
|
||||
},
|
||||
globals: {
|
||||
// chai tests
|
||||
assert: true,
|
||||
expect: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import BInput from "bootstrap-vue/es/components/form-input/form-input";
|
||||
import { createMask } from "imask";
|
||||
|
||||
export default {
|
||||
extends: BInput,
|
||||
props: {
|
||||
mask: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
masker() {
|
||||
return createMask({
|
||||
mask: this.mask
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFormatted(value) {
|
||||
const result = this.stringifyValue(value);
|
||||
if (this.mask.length) {
|
||||
return this.masker.resolve(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import sinon from "sinon";
|
||||
import flushPromises from "flush-promises";
|
||||
import { shallowMount, createLocalVue } from "@vue/test-utils";
|
||||
|
||||
import { default as CloudAuth, __RewireAPI__ as rewire } from "./CloudAuth";
|
||||
import CloudAuthItem from "./CloudAuthItem";
|
||||
import { Credential } from "./model";
|
||||
|
||||
import _l from "utils/localization";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
// test data
|
||||
import listCredentials from "./testdata/listCredentials.json";
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(BootstrapVue);
|
||||
localVue.filter("localize", value => _l(value));
|
||||
|
||||
describe("CloudAuth component", () => {
|
||||
let stub, wrapper;
|
||||
|
||||
let mockSvc = {
|
||||
listCredentials: async () => null
|
||||
};
|
||||
|
||||
rewire.__Rewire__("svc", mockSvc);
|
||||
|
||||
beforeEach(async () => {
|
||||
let creds = listCredentials.map(Credential.create);
|
||||
stub = sinon.stub(mockSvc, "listCredentials").resolves(creds);
|
||||
wrapper = shallowMount(CloudAuth, { localVue });
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (stub) {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should render the initial list", () => {
|
||||
assert(wrapper);
|
||||
assert(wrapper.contains(CloudAuthItem));
|
||||
assert(wrapper.vm.items.length == 2);
|
||||
assert(wrapper.vm.filteredItems.length == 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("text filter", () => {
|
||||
it("should show filter result by text match", () => {
|
||||
let results;
|
||||
|
||||
wrapper.vm.filter = "aws";
|
||||
results = wrapper.vm.filteredItems;
|
||||
assert(wrapper.contains(CloudAuthItem));
|
||||
assert(results.length == 1, `Wrong number of items: ${results.length}`);
|
||||
|
||||
wrapper.vm.filter = "azure";
|
||||
results = wrapper.vm.filteredItems;
|
||||
assert(results.length == 1, `Wrong number of items: ${results.length}`);
|
||||
|
||||
wrapper.vm.filter = "";
|
||||
results = wrapper.vm.filteredItems;
|
||||
assert(results.length == 2, `Wrong number of items: ${results.length}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("create button", () => {
|
||||
it("clicking create button should add a blank key", () => {
|
||||
let results = wrapper.vm.filteredItems;
|
||||
assert(wrapper.contains(CloudAuthItem));
|
||||
assert(results.length == 2, `Wrong number of items: ${results.length}`);
|
||||
|
||||
let button = wrapper.find("button[name=createNewKey]");
|
||||
assert(button);
|
||||
button.trigger("click");
|
||||
|
||||
results = wrapper.vm.filteredItems;
|
||||
assert(results.length == 3, `Wrong number of items: ${results.length}`);
|
||||
|
||||
let blank = results.find(i => i.id == null);
|
||||
assert(blank, "missing blank key");
|
||||
assert(blank.id == null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<section class="cloud-auth">
|
||||
<header>
|
||||
<b-alert dismissible fade variant="warning" :show="errorMessage !== null" @dismissed="errorMessage = null">
|
||||
{{ errorMessage }}
|
||||
</b-alert>
|
||||
|
||||
<hgroup class="cloud-auth-title">
|
||||
<h1>Manage Cloud Authorization</h1>
|
||||
<nav class="operations">
|
||||
<ul>
|
||||
<li class="cloudKeyHelp">
|
||||
<a
|
||||
:class="{ active: showHelp }"
|
||||
@click.prevent="showHelp = !showHelp"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Instructions"
|
||||
title="Instructions"
|
||||
>
|
||||
<span>Instructions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="cloudKeyFilter">
|
||||
<a
|
||||
:class="{ active: showFilter }"
|
||||
@click.prevent="showFilter = !showFilter"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Filter Results"
|
||||
title="Filter Results"
|
||||
>
|
||||
<span>Filter</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="createCloudKey">
|
||||
<a
|
||||
@click.prevent="onCreate"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Create New Key"
|
||||
title="Create New Key"
|
||||
>
|
||||
<span>Create New Key</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</hgroup>
|
||||
|
||||
<transition name="fade">
|
||||
<hgroup v-if="showHelp">
|
||||
<p>See the online <a href="https://galaxyproject.org/cloud/authnz/">Documentation</a>.</p>
|
||||
</hgroup>
|
||||
</transition>
|
||||
|
||||
<transition name="fade">
|
||||
<hgroup v-if="showFilter">
|
||||
<b-form-group :description="filterDescription">
|
||||
<b-form-input type="text" placeholder="Filter" v-model="filter" />
|
||||
</b-form-group>
|
||||
</hgroup>
|
||||
</transition>
|
||||
|
||||
<b-button-group class="mb-4">
|
||||
<button name="createNewKey" @click="onCreate">
|
||||
Create New Authorization Key
|
||||
</button>
|
||||
</b-button-group>
|
||||
</header>
|
||||
|
||||
<div class="scroll-container">
|
||||
<b-list-group>
|
||||
<transition-group name="fade">
|
||||
<cloud-auth-item
|
||||
v-for="credential in filteredItems"
|
||||
:key="credential.counter"
|
||||
:credential="credential"
|
||||
class="mb-1"
|
||||
@delete="onDelete"
|
||||
@save="onSave"
|
||||
@expand="expand(credential, $event)"
|
||||
/>
|
||||
</transition-group>
|
||||
</b-list-group>
|
||||
</div>
|
||||
|
||||
<b-modal
|
||||
v-model="hasDoomed"
|
||||
centered
|
||||
id="deleteCredentialModal"
|
||||
ref="deleteModal"
|
||||
title="Delete Key?"
|
||||
size="sm"
|
||||
@ok="deleteKey"
|
||||
@cancel="doomedItem = null"
|
||||
>
|
||||
</b-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import CloudAuthItem from "./CloudAuthItem";
|
||||
import { Credential } from "./model";
|
||||
import svc from "./model/service";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CloudAuthItem
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
filter: "",
|
||||
showHelp: true,
|
||||
showFilter: false,
|
||||
loading: false,
|
||||
doomedItem: null,
|
||||
errorMessage: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredItems() {
|
||||
return this.items.filter(o => o.match(this.filter));
|
||||
},
|
||||
filterDescription() {
|
||||
return `${this.filteredItems.length} matches out of ${this.items.length} items`;
|
||||
},
|
||||
deleteButtonVariant() {
|
||||
return this.showDeleted ? "primary" : "secondary";
|
||||
},
|
||||
hasDoomed: {
|
||||
get() {
|
||||
return this.doomedItem !== null;
|
||||
},
|
||||
// This setter is here because vue-bootstrap modal
|
||||
// tries to set this property for unfathomable reasons
|
||||
set() {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showDeleted(deleted) {
|
||||
this.loadCredentials({ deleted });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadCredentials(params = {}) {
|
||||
this.loading = true;
|
||||
svc.listCredentials(params)
|
||||
.then(items => (this.items = items))
|
||||
.catch(this.setError("Unable to load cloud keys."))
|
||||
.finally(() => (this.loading = false));
|
||||
},
|
||||
onCreate() {
|
||||
const newItem = Credential.create({ expanded: true });
|
||||
this.addItem(newItem);
|
||||
},
|
||||
onSave(item) {
|
||||
if (!item.valid) {
|
||||
return;
|
||||
}
|
||||
item.loading = true;
|
||||
svc.saveCredential(item)
|
||||
.then(result => {
|
||||
item.id = result.id;
|
||||
item.updateState();
|
||||
})
|
||||
.catch(this.setError("Unable to save key."))
|
||||
.finally(() => (item.loading = false));
|
||||
},
|
||||
onDelete(doomed) {
|
||||
this.doomedItem = doomed;
|
||||
if (doomed.id) {
|
||||
this.$refs.deleteModal.show();
|
||||
} else {
|
||||
this.removeItem(doomed);
|
||||
this.doomedItem = null;
|
||||
}
|
||||
},
|
||||
deleteKey() {
|
||||
// Called when the modal is closed with an "OK"
|
||||
svc.deleteCredential(this.doomedItem)
|
||||
.then(() => this.removeItem(this.doomedItem))
|
||||
.catch(this.setError("Unable to delete cloud key."))
|
||||
.finally(() => {
|
||||
this.doomedItem = null;
|
||||
});
|
||||
},
|
||||
expand(credential, { expanded }) {
|
||||
credential.expanded = expanded;
|
||||
if (expanded) {
|
||||
this.items.filter(i => i !== credential).forEach(i => (i.expanded = false));
|
||||
}
|
||||
},
|
||||
addItem(item) {
|
||||
this.items = [...this.items, item];
|
||||
},
|
||||
removeItem(item) {
|
||||
this.items = this.items.filter(o => o !== item);
|
||||
},
|
||||
setError(msg) {
|
||||
return err => {
|
||||
this.errorMessage = msg;
|
||||
console.warn(err);
|
||||
};
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadCredentials();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
@import "~bootstrap/scss/mixins";
|
||||
@import "~bootstrap/scss/utilities/spacing";
|
||||
|
||||
@import "scss/theme/blue.scss";
|
||||
@import "scss/mixins";
|
||||
|
||||
// TODO: build reusable icon menu? Maybe make into a component?
|
||||
// The existing one has lots of reusability problems
|
||||
|
||||
.operations {
|
||||
margin-bottom: 0;
|
||||
|
||||
ul {
|
||||
@include list_reset();
|
||||
display: flex;
|
||||
li:not(:first-child) {
|
||||
@extend .ml-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// General Layout
|
||||
|
||||
.cloud-auth {
|
||||
// header sticks, bottom part scrolls
|
||||
@include scrollingListLayout("header", ".scroll-container");
|
||||
|
||||
// title left, icons right
|
||||
.cloud-auth-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
// top icon menu
|
||||
.operations {
|
||||
li {
|
||||
a,
|
||||
a::before {
|
||||
font-size: 1rem;
|
||||
color: $gray-400;
|
||||
}
|
||||
a.active::before,
|
||||
a:hover::before {
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.cloudKeyHelp a {
|
||||
@include fontawesome($fa-var-info-circle);
|
||||
}
|
||||
.cloudKeyFilter a {
|
||||
@include fontawesome($fa-var-filter);
|
||||
}
|
||||
.createCloudKey a {
|
||||
@include fontawesome($fa-var-plus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Single list item
|
||||
|
||||
.cloud-auth-key {
|
||||
header hgroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
@extend .my-3;
|
||||
@extend .pt-3;
|
||||
|
||||
// removes wierd double arrows on select
|
||||
.custom-select {
|
||||
background: none;
|
||||
}
|
||||
|
||||
// Undo butchering from base.css so side-by-side labels work
|
||||
.form-row {
|
||||
display: flex;
|
||||
input,
|
||||
select {
|
||||
max-width: none;
|
||||
}
|
||||
label {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
// button list at bottom of form
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
@extend .pt-3;
|
||||
|
||||
button:not(:first-child) {
|
||||
@extend .ml-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// icon menu
|
||||
|
||||
.operations {
|
||||
.save a {
|
||||
@include fontawesome($fa-var-save);
|
||||
}
|
||||
.delete a {
|
||||
@include fontawesome($fa-var-times);
|
||||
}
|
||||
.details a {
|
||||
@include fontawesome($fa-var-window-minimize);
|
||||
&:hover {
|
||||
@include fontawesome($fa-var-window-maximize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reverse icons when key is expanded
|
||||
|
||||
&.expanded .details a {
|
||||
@include fontawesome($fa-var-window-maximize);
|
||||
&:hover {
|
||||
@include fontawesome($fa-var-window-minimize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transitions
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Delete modal
|
||||
|
||||
#deleteCredentialModal {
|
||||
.modal-body {
|
||||
display: none;
|
||||
}
|
||||
.modal-dialog {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<b-list-group-item class="cloud-auth-key" :class="statusClasses" :variant="variant" :button="!expanded">
|
||||
<header>
|
||||
<hgroup>
|
||||
<h4 @click.prevent="expand()">{{ credential.title }}</h4>
|
||||
<nav class="operations">
|
||||
<ul>
|
||||
<transition name="fade">
|
||||
<li class="save" v-if="expanded && credential.dirty">
|
||||
<a
|
||||
@click.prevent="$emit('save', credential)"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Save Key"
|
||||
title="Save Key"
|
||||
>
|
||||
<span>Save Key</span>
|
||||
</a>
|
||||
</li>
|
||||
</transition>
|
||||
<li class="delete">
|
||||
<a
|
||||
@click.prevent="$emit('delete', credential)"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Delete Key"
|
||||
title="Delete Key"
|
||||
>
|
||||
<span>Delete Key</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="details">
|
||||
<a
|
||||
@click.prevent="expand()"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Show Details"
|
||||
title="Show Details"
|
||||
>
|
||||
<span>Details</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<credential-form
|
||||
v-if="expanded"
|
||||
class="border-top"
|
||||
v-model="credential"
|
||||
@click.self="expand()"
|
||||
@save="$emit('save', credential)"
|
||||
@delete="$emit('delete', credential)"
|
||||
/>
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Credential } from "./model";
|
||||
import CredentialForm from "./CredentialForm";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CredentialForm
|
||||
},
|
||||
props: {
|
||||
credential: { type: Credential, required: true }
|
||||
},
|
||||
computed: {
|
||||
statusClasses() {
|
||||
const { expanded, valid, dirty, loading } = this.credential;
|
||||
return { loading, expanded, valid, dirty, collapsed: !expanded };
|
||||
},
|
||||
variant() {
|
||||
if (this.expanded) {
|
||||
return "";
|
||||
}
|
||||
if (this.credential.dirty) {
|
||||
return "warning";
|
||||
}
|
||||
if (!this.credential.valid) {
|
||||
return "danger";
|
||||
}
|
||||
return "primary";
|
||||
},
|
||||
expanded() {
|
||||
return this.credential.expanded;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
expand(forceState) {
|
||||
const expanded = forceState !== undefined ? forceState : !this.expanded;
|
||||
this.$emit("expand", { expanded });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
v-for="(field, fieldName) in fieldDefs"
|
||||
:key="fieldName"
|
||||
:label="field.label || fieldName"
|
||||
:label-for="'config-' + fieldName"
|
||||
label-cols-lg="3"
|
||||
:state="config.fieldValid(fieldName)"
|
||||
:invalid-feedback="config.errorMessage(fieldName)"
|
||||
>
|
||||
<masked-input
|
||||
:id="'config-' + fieldName"
|
||||
v-model="config[fieldName]"
|
||||
:mask="field.mask"
|
||||
:placeholder="field.placeholder"
|
||||
:maxlength="field.maxlength || Math.Infinity"
|
||||
:state="config.fieldValid(fieldName)"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MaskedInput from "components/MaskedInput";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MaskedInput
|
||||
},
|
||||
props: {
|
||||
value: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.value;
|
||||
},
|
||||
fieldDefs() {
|
||||
return this.value.constructor.fields;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<b-form :validated="credential.isValid">
|
||||
<!-- user label -->
|
||||
<b-form-group label="Description" label-for="credentialDescription" label-cols-lg="3">
|
||||
<b-form-input
|
||||
id="credentialDescription"
|
||||
v-model="credential.description"
|
||||
:state="credential.fieldValid('description')"
|
||||
placeholder="Description (optional)"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- google-openidconnect -->
|
||||
<b-form-group
|
||||
label="Identity Provider"
|
||||
label-for="identityProvider"
|
||||
label-cols-lg="3"
|
||||
v-if="identityProviders.length > 1"
|
||||
>
|
||||
<b-form-select
|
||||
id="identityProvider"
|
||||
v-model="credential.authn_id"
|
||||
:state="credential.fieldValid('authn_id')"
|
||||
:options="identityProviders"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- aws/azure, etc -->
|
||||
<b-form-group label="Resource Provider" label-for="resourceProvider" label-cols-lg="3">
|
||||
<b-form-select
|
||||
id="resourceProvider"
|
||||
v-model="credential.resourceProvider"
|
||||
:state="credential.fieldValid('provider')"
|
||||
:options="resourceProviderOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<credential-config v-model="credential.config" />
|
||||
|
||||
<footer class="border-top">
|
||||
<b-button variant="secondary" @click.prevent="$emit('delete', credential)">
|
||||
Delete Key
|
||||
</b-button>
|
||||
<b-button
|
||||
aria-label="Save Key"
|
||||
:variant="saveButtonVariant"
|
||||
:disabled="saveButtonDisabled"
|
||||
@click.prevent="$emit('save', credential)"
|
||||
>
|
||||
{{ saveButtonTitle }}
|
||||
</b-button>
|
||||
</footer>
|
||||
</b-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Credential, ResourceProviders } from "./model";
|
||||
import { getIdentityProviders } from "./model/service";
|
||||
import CredentialConfig from "./CredentialConfig";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CredentialConfig
|
||||
},
|
||||
props: {
|
||||
value: { type: Credential, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
identityProviders: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
credential() {
|
||||
return this.value;
|
||||
},
|
||||
// transformed for bootstrap component
|
||||
resourceProviderOptions() {
|
||||
return Array.from(ResourceProviders.entries()).map(([value, o]) => ({ value, text: o.label }));
|
||||
},
|
||||
config() {
|
||||
return this.credential.config;
|
||||
},
|
||||
loading() {
|
||||
return this.credential.loading;
|
||||
},
|
||||
saveButtonDisabled() {
|
||||
return !(this.credential.valid && this.credential.dirty);
|
||||
},
|
||||
saveButtonVariant() {
|
||||
return this.saveButtonDisabled ? "secondary" : "primary";
|
||||
},
|
||||
saveButtonTitle() {
|
||||
return this.loading ? "Saving Key..." : "Save Key";
|
||||
}
|
||||
},
|
||||
created() {
|
||||
getIdentityProviders().then(result => {
|
||||
if (!this.credential.authn_id && result.length == 1) {
|
||||
this.credential.authn_id = result[0].authn_id;
|
||||
this.credential.updateState();
|
||||
}
|
||||
this.identityProviders = result;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CloudAuth } from "./CloudAuth.vue";
|
||||
@@ -0,0 +1,27 @@
|
||||
import { safeAssign } from "utils/safeAssign";
|
||||
import { BaseModel } from "./BaseModel";
|
||||
|
||||
export class AwsConfig extends BaseModel {
|
||||
constructor(props = {}) {
|
||||
super();
|
||||
this.role_arn = "";
|
||||
safeAssign(this, props);
|
||||
this.updateState();
|
||||
}
|
||||
}
|
||||
|
||||
AwsConfig.setValidator(function(model) {
|
||||
const errors = {};
|
||||
if (!model.role_arn.length) {
|
||||
errors.role_arn = "Missing role_arn";
|
||||
}
|
||||
return errors;
|
||||
});
|
||||
|
||||
AwsConfig.fields = {
|
||||
role_arn: {
|
||||
label: "Role ARN",
|
||||
description: "The Amazon resource name (ARN) of the role to be assumed by Galaxy.",
|
||||
placeholder: "arn:aws:iam::XXXXXXXXXXXX:role/XXXXXXXXXXX"
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AwsConfig } from "./AwsConfig";
|
||||
|
||||
describe("AwsConfig", () => {
|
||||
it("should instantiate", () => {
|
||||
let instance = new AwsConfig();
|
||||
assert(instance);
|
||||
assert(!instance.dirty);
|
||||
assert(!instance.valid);
|
||||
});
|
||||
|
||||
it("should validate role_arn", () => {
|
||||
let instance = new AwsConfig();
|
||||
instance.role_arn = "abc";
|
||||
assert(instance.fieldValid("role_arn"));
|
||||
});
|
||||
|
||||
it("should invalidate role_arn", () => {
|
||||
let instance = new AwsConfig();
|
||||
instance.role_arn = "";
|
||||
assert(!instance.fieldValid("role_arn"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { safeAssign } from "utils/safeAssign";
|
||||
import { BaseModel } from "./BaseModel";
|
||||
|
||||
export class AzureConfig extends BaseModel {
|
||||
constructor(props = {}) {
|
||||
super();
|
||||
this.tenant_id = "";
|
||||
this.client_id = "";
|
||||
this.client_secret = "";
|
||||
safeAssign(this, props);
|
||||
this.updateState();
|
||||
}
|
||||
}
|
||||
|
||||
AzureConfig.setValidator(function(model) {
|
||||
const errors = {};
|
||||
|
||||
if (model.tenant_id.length < 36) {
|
||||
errors.tenant_id = "Tenant ID too short";
|
||||
}
|
||||
if (!model.tenant_id) {
|
||||
errors.tenant_id = "Missing Tenant ID";
|
||||
}
|
||||
|
||||
if (model.client_id.length < 36) {
|
||||
errors.client_id = "Client ID too short";
|
||||
}
|
||||
if (!model.client_id) {
|
||||
errors.client_id = "Missing client ID";
|
||||
}
|
||||
|
||||
if (!model.client_secret) {
|
||||
errors.client_secret = "Missing secret";
|
||||
}
|
||||
|
||||
return errors;
|
||||
});
|
||||
|
||||
AzureConfig.fields = {
|
||||
tenant_id: {
|
||||
label: "Tenant ID",
|
||||
mask: "********-****-****-****-************",
|
||||
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
maxlength: 36,
|
||||
description: "Your Tenant ID (or Directory ID) on Azure."
|
||||
},
|
||||
client_id: {
|
||||
label: "Client ID",
|
||||
mask: "********-****-****-****-************",
|
||||
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
maxlength: 36,
|
||||
description: "The Client ID (or Application ID) you defined for Galaxy on your Azure directory."
|
||||
},
|
||||
client_secret: {
|
||||
label: "Client Secret",
|
||||
mask: "",
|
||||
placeholder: "Client Secret",
|
||||
description:
|
||||
"A secret string you obtained from Azure portal that Galaxy can use to prove its identity when requesting tokens to access your resources."
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { AzureConfig } from "./AzureConfig";
|
||||
|
||||
describe("AzureConfig", () => {
|
||||
it("should instantiate", () => {
|
||||
let instance = new AzureConfig();
|
||||
assert(instance);
|
||||
assert(!instance.dirty);
|
||||
assert(!instance.valid);
|
||||
});
|
||||
|
||||
describe("client_secret", () => {
|
||||
it("should validate client_secret", () => {
|
||||
let instance = new AzureConfig();
|
||||
instance.client_secret = "abc";
|
||||
assert(instance.fieldValid("client_secret"));
|
||||
});
|
||||
|
||||
it("should invalidate client_secret", () => {
|
||||
let instance = new AzureConfig();
|
||||
instance.client_secret = "";
|
||||
assert(!instance.fieldValid("client_secret"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("tenant_id", () => {
|
||||
it("should validate tenant_id", () => {
|
||||
let validTenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
|
||||
let instance = new AzureConfig();
|
||||
instance.tenant_id = validTenantId;
|
||||
assert(instance.fieldValid("tenant_id"));
|
||||
});
|
||||
|
||||
it("should invalidate tenant_id", () => {
|
||||
let instance = new AzureConfig();
|
||||
instance.tenant_id = "";
|
||||
assert(!instance.fieldValid("tenant_id"));
|
||||
instance.tenant_id = "asdfa";
|
||||
assert(!instance.fieldValid("tenant_id"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("client_id", () => {
|
||||
it("should validate client_id", () => {
|
||||
let validClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
|
||||
let instance = new AzureConfig();
|
||||
instance.client_id = validClientId;
|
||||
assert(instance.fieldValid("client_id"));
|
||||
});
|
||||
|
||||
it("should invalidate client_id", () => {
|
||||
let instance = new AzureConfig();
|
||||
instance.client_id = "";
|
||||
assert(!instance.fieldValid("client_id"));
|
||||
instance.client_id = "asdf";
|
||||
assert(!instance.fieldValid("client_id"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
const lastState = new WeakMap();
|
||||
const validators = new WeakMap();
|
||||
const transients = new WeakMap();
|
||||
const counter = new WeakMap();
|
||||
|
||||
// Helps with Vue for-loop keys
|
||||
let instanceCounter = 0;
|
||||
|
||||
export class BaseModel {
|
||||
constructor() {
|
||||
counter.set(this, instanceCounter++);
|
||||
}
|
||||
|
||||
/* object ID */
|
||||
|
||||
get counter() {
|
||||
return counter.get(this);
|
||||
}
|
||||
|
||||
/* Dirty state tracking */
|
||||
|
||||
get dirty() {
|
||||
return lastState.get(this) != this.state;
|
||||
}
|
||||
|
||||
get clean() {
|
||||
return !this.dirty;
|
||||
}
|
||||
|
||||
get state() {
|
||||
// build state JSON ignoring transient fields
|
||||
const tFields = this.transientFields;
|
||||
return JSON.stringify(this, function(key) {
|
||||
if (!tFields.has(key)) {
|
||||
return this[key];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
get lastState() {
|
||||
return lastState.get(this);
|
||||
}
|
||||
|
||||
get transientFields() {
|
||||
const key = this.constructor;
|
||||
if (!transients.has(key)) {
|
||||
transients.set(key, new Set());
|
||||
}
|
||||
return transients.get(key);
|
||||
}
|
||||
|
||||
updateState() {
|
||||
lastState.set(this, this.state);
|
||||
}
|
||||
|
||||
// Setting a property name as transient for a class means its
|
||||
// state will be caclulated without regard to that propety
|
||||
static setTransient(...fieldNames) {
|
||||
const klass = this; // this will be a class
|
||||
if (!transients.has(klass)) {
|
||||
transients.set(klass, new Set());
|
||||
}
|
||||
|
||||
const fields = transients.get(klass);
|
||||
fieldNames.forEach(fieldName => fields.add(fieldName));
|
||||
transients.set(klass, fields);
|
||||
}
|
||||
|
||||
/* Validation */
|
||||
|
||||
get valid() {
|
||||
return Object.keys(this.validationErrors).length == 0;
|
||||
}
|
||||
|
||||
get validationErrors() {
|
||||
return this.constructor.validate(this);
|
||||
}
|
||||
|
||||
errorMessage(field) {
|
||||
if (field in this.validationErrors) {
|
||||
return this.validationErrors[field];
|
||||
}
|
||||
return "errmessage";
|
||||
}
|
||||
|
||||
fieldValid(fieldName) {
|
||||
return !(fieldName in this.validationErrors);
|
||||
}
|
||||
|
||||
static validate(model) {
|
||||
if (validators.has(this)) {
|
||||
const validator = validators.get(this);
|
||||
return validator(model);
|
||||
}
|
||||
throw new Error("Missing validator");
|
||||
}
|
||||
|
||||
static setValidator(validationFunction) {
|
||||
validators.set(this, validationFunction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { safeAssign } from "utils/safeAssign";
|
||||
import { ResourceProviders } from "./ResourceProviders";
|
||||
import { BaseModel } from "./BaseModel";
|
||||
|
||||
export class Credential extends BaseModel {
|
||||
constructor(props = {}) {
|
||||
super();
|
||||
|
||||
this.id = null;
|
||||
this.description = "";
|
||||
this.authn_id = null; // identity provider
|
||||
this.provider = null; // resource provider
|
||||
|
||||
// transient props, exclude from state
|
||||
this.expanded = false;
|
||||
this.loading = false;
|
||||
|
||||
// populate props
|
||||
const options = Object.assign({}, Credential.defaults, props);
|
||||
safeAssign(this, options);
|
||||
|
||||
// init nested config
|
||||
this.config = new this.configClass(options.config);
|
||||
|
||||
// initialize state
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.description.length ? this.description : this.provider;
|
||||
}
|
||||
|
||||
get valid() {
|
||||
return super.valid && this.config.valid;
|
||||
}
|
||||
|
||||
// Alias for provider also changes config object when updated
|
||||
// Set this when updating in the UI
|
||||
|
||||
get resourceProvider() {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
set resourceProvider(newProvider) {
|
||||
this.provider = newProvider;
|
||||
this.config = new this.configClass({});
|
||||
}
|
||||
|
||||
// Polymorphic config class
|
||||
|
||||
get configClass() {
|
||||
return ResourceProviders.get(this.provider).klass;
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
match(searchText = "") {
|
||||
// TODO: more robust object matching?
|
||||
return searchText.length ? this.title.includes(searchText) : true;
|
||||
}
|
||||
|
||||
// Statics
|
||||
|
||||
static get defaults() {
|
||||
return {
|
||||
authn_id: null,
|
||||
provider: "aws",
|
||||
expanded: false
|
||||
};
|
||||
}
|
||||
|
||||
static create(props = {}) {
|
||||
return new Credential(props);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transient fields do not factor into the state
|
||||
* for purposes of dirty tracking
|
||||
*/
|
||||
Credential.setTransient("expanded", "loading");
|
||||
|
||||
/**
|
||||
* A validator function's job is to return an object
|
||||
* where the keys are field names and the values are
|
||||
* error messages.
|
||||
*/
|
||||
Credential.setValidator(function(model) {
|
||||
const errors = {};
|
||||
|
||||
if (!model.provider) {
|
||||
errors.provider = "Provider must be set";
|
||||
}
|
||||
|
||||
if (!model.authn_id) {
|
||||
errors.authn_id = "Please pick an identity provider";
|
||||
}
|
||||
|
||||
if (!model.config.valid) {
|
||||
errors.config = "Invalid config object";
|
||||
}
|
||||
|
||||
return errors;
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Credential } from "./Credential";
|
||||
|
||||
describe("Credential model", () => {
|
||||
describe("basic model props", () => {
|
||||
it("should exist", () => {
|
||||
assert(Credential);
|
||||
});
|
||||
|
||||
it("should build with defaults", () => {
|
||||
let instance = new Credential();
|
||||
assert(instance);
|
||||
});
|
||||
|
||||
it("should build with props", () => {
|
||||
let description = "i am the test description";
|
||||
let props = { description };
|
||||
let instance = new Credential(props);
|
||||
assert(instance);
|
||||
assert(instance.description == description);
|
||||
});
|
||||
|
||||
it("default config object should be AWS", () => {
|
||||
let instance = new Credential();
|
||||
assert("role_arn" in instance.config, "Missing role_arn prop");
|
||||
assert(instance.config.constructor.name == "AwsConfig");
|
||||
});
|
||||
|
||||
it("should load a different config object given the right provider", () => {
|
||||
let provider = "azure";
|
||||
let props = { provider };
|
||||
let instance = new Credential(props);
|
||||
assert(instance.provider == provider);
|
||||
assert(instance.config.constructor.name == "AzureConfig");
|
||||
});
|
||||
|
||||
it("should dynamically switch config objects as resourceProvider is changed", () => {
|
||||
let instance = new Credential();
|
||||
assert(instance.provider == "aws", "Should default to AWS config' ");
|
||||
instance.resourceProvider = "azure";
|
||||
assert(instance.config.constructor.name == "AzureConfig", "Should have been AzureConfig");
|
||||
instance.resourceProvider = "aws";
|
||||
assert(instance.config.constructor.name == "AwsConfig", "Should be AWS Config again");
|
||||
});
|
||||
});
|
||||
|
||||
describe("dirty state", () => {
|
||||
it("should flag as dirty when a prop is changed", () => {
|
||||
let instance = new Credential();
|
||||
instance.description = "foo";
|
||||
assert(instance.dirty);
|
||||
});
|
||||
|
||||
it("should flag as clean when a prop is restored", () => {
|
||||
let instance = new Credential();
|
||||
instance.description = "foo";
|
||||
assert(instance.dirty);
|
||||
instance.description = "";
|
||||
assert(!instance.dirty);
|
||||
});
|
||||
|
||||
it("should not flag as dirty when a transient prop is changed", () => {
|
||||
let instance = new Credential();
|
||||
instance.loading = true;
|
||||
assert(!instance.dirty);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validation", () => {
|
||||
it("a new object should be invalid", () => {
|
||||
let instance = new Credential();
|
||||
assert(!instance.valid);
|
||||
});
|
||||
|
||||
it("it should become valid when props are assigned", () => {
|
||||
let instance = new Credential();
|
||||
instance.authn_id = "floob";
|
||||
instance.resourceProvider = "aws";
|
||||
instance.config.role_arn = "abc";
|
||||
assert(instance.valid);
|
||||
});
|
||||
|
||||
it("it should be valid when initialized with correct props", () => {
|
||||
let props = {
|
||||
authn_id: "asdfasdf",
|
||||
provider: "aws",
|
||||
config: {
|
||||
role_arn: "floobar"
|
||||
}
|
||||
};
|
||||
let instance = new Credential(props);
|
||||
assert(instance.valid);
|
||||
});
|
||||
|
||||
it("should be invalid when bad props assigned", () => {
|
||||
let props = {
|
||||
authn_id: "asdfasdf",
|
||||
provider: "aws",
|
||||
config: {
|
||||
role_arn: ""
|
||||
}
|
||||
};
|
||||
let instance = new Credential(props);
|
||||
assert(!instance.valid);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { safeAssign } from "utils/safeAssign";
|
||||
|
||||
export class IdentityProvider {
|
||||
constructor(props = {}) {
|
||||
this.id = "";
|
||||
this.provider = "";
|
||||
safeAssign(this, props);
|
||||
}
|
||||
|
||||
// Aliases
|
||||
|
||||
get authn_id() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
// Statics
|
||||
|
||||
static create(props = {}) {
|
||||
return new IdentityProvider(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AwsConfig } from "./AwsConfig";
|
||||
import { AzureConfig } from "./AzureConfig";
|
||||
|
||||
export const ResourceProviders = new Map();
|
||||
|
||||
ResourceProviders.set("aws", {
|
||||
klass: AwsConfig,
|
||||
label: "Amazon Web Services (AWS)"
|
||||
});
|
||||
|
||||
ResourceProviders.set("azure", {
|
||||
klass: AzureConfig,
|
||||
label: "Microsoft Azure"
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export { Credential } from "./Credential";
|
||||
export { IdentityProvider } from "./IdentityProvider";
|
||||
export { ResourceProviders } from "./ResourceProviders";
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Data retrieval/storage for the auth keys
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { Credential, IdentityProvider } from "./index";
|
||||
import { getRootFromIndexLink } from "onload";
|
||||
|
||||
const getUrl = path => getRootFromIndexLink() + path;
|
||||
|
||||
export async function listCredentials() {
|
||||
const url = getUrl("api/cloud/authz");
|
||||
const response = await axios.get(url);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Unexpected response from listing.");
|
||||
}
|
||||
return response.data.map(Credential.create);
|
||||
}
|
||||
|
||||
export async function getCredential(id) {
|
||||
const url = getUrl("api/cloud/authz/${id}");
|
||||
const response = await axios.get(url);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Unexpected response loading key.");
|
||||
}
|
||||
return Credential.create(response.data);
|
||||
}
|
||||
|
||||
export async function saveCredential(newItem) {
|
||||
const model = Credential.create(newItem);
|
||||
const response = await saveOrUpdate(model);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Save failure.");
|
||||
}
|
||||
return Credential.create(response.data);
|
||||
}
|
||||
|
||||
async function saveOrUpdate(model) {
|
||||
return model.id
|
||||
? axios.put(getUrl(`api/cloud/authz/${model.id}`), model)
|
||||
: axios.post(getUrl("api/cloud/authz"), model);
|
||||
}
|
||||
|
||||
export async function deleteCredential(doomed) {
|
||||
const model = Credential.create(doomed);
|
||||
if (model.id) {
|
||||
const url = getUrl(`api/cloud/authz/${doomed.id}`);
|
||||
const response = await axios.delete(url);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Delete failure.");
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// Memoize results (basically never changes)
|
||||
|
||||
let identityProviders;
|
||||
|
||||
export async function getIdentityProviders() {
|
||||
if (!identityProviders) {
|
||||
const url = getUrl("authnz");
|
||||
const response = await axios.get(url);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Unable to load identity providers");
|
||||
}
|
||||
identityProviders = response.data.map(IdentityProvider.create);
|
||||
}
|
||||
return identityProviders;
|
||||
}
|
||||
|
||||
export default {
|
||||
listCredentials,
|
||||
getCredential,
|
||||
saveCredential,
|
||||
deleteCredential,
|
||||
getIdentityProviders
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"authn_id": "f2db41e1fa331b3e",
|
||||
"user_id": "f2db41e1fa331b3e",
|
||||
"description": "aws description",
|
||||
"last_update": "2019-04-04 09:13:16.135032",
|
||||
"last_activity": "2019-04-04 09:13:16.135042",
|
||||
"create_time": "2019-04-04 16:13:16.135755",
|
||||
"provider": "aws",
|
||||
"model_class": "CloudAuthz",
|
||||
"config": {
|
||||
"role_arn": "floobadooba"
|
||||
},
|
||||
"id": "a7db2fac67043c7e"
|
||||
},
|
||||
{
|
||||
"authn_id": "f2db41e1fa331b3e",
|
||||
"user_id": "f2db41e1fa331b3e",
|
||||
"description": "azure description",
|
||||
"last_update": "2019-04-03 20:21:51.350748",
|
||||
"last_activity": "2019-04-03 20:21:51.350804",
|
||||
"create_time": "2019-04-04 03:21:51.351508",
|
||||
"provider": "azure",
|
||||
"model_class": "CloudAuthz",
|
||||
"config": {
|
||||
"client_secret": "dannon likes Nickleback",
|
||||
"client_id": "22222222-2222-2222-2222-222222222222",
|
||||
"tenant_id": "11111111-1111-1111-1111-111111111111"
|
||||
},
|
||||
"id": "b472e2eb553fa0d1"
|
||||
}
|
||||
]
|
||||
@@ -38,6 +38,7 @@ import DatasetEditAttributes from "mvc/dataset/dataset-edit-attributes";
|
||||
import Citations from "components/Citations.vue";
|
||||
import DisplayStructure from "components/DisplayStructured.vue";
|
||||
import Vue from "vue";
|
||||
import { CloudAuth } from "components/User/CloudAuth";
|
||||
|
||||
/** Routes */
|
||||
export const getAnalysisRouter = Galaxy =>
|
||||
@@ -47,6 +48,7 @@ export const getAnalysisRouter = Galaxy =>
|
||||
"(/)root*": "home",
|
||||
"(/)tours(/)(:tour_id)": "show_tours",
|
||||
"(/)user(/)": "show_user",
|
||||
"(/)user(/)cloud_auth": "show_cloud_auth",
|
||||
"(/)user(/)(:form_id)": "show_user_form",
|
||||
"(/)pages(/)create(/)": "show_pages_create",
|
||||
"(/)pages(/)edit(/)": "show_pages_edit",
|
||||
@@ -75,18 +77,19 @@ export const getAnalysisRouter = Galaxy =>
|
||||
"(/)datasets/error": "show_dataset_error"
|
||||
},
|
||||
|
||||
require_login: ["show_user", "show_user_form", "show_workflows"],
|
||||
require_login: ["show_user", "show_user_form", "show_workflows", "show_cloud_auth"],
|
||||
|
||||
authenticate: function(args, name) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return (Galaxy.user && Galaxy.user.id) || this.require_login.indexOf(name) == -1;
|
||||
},
|
||||
|
||||
_display_vue_helper: function(component, props) {
|
||||
_display_vue_helper: function(component, props = {}) {
|
||||
const instance = Vue.extend(component);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new instance(props).$mount(vm);
|
||||
const container = document.createElement("div");
|
||||
this.page.display(container);
|
||||
const vm = new instance(props).$mount(container);
|
||||
return vm;
|
||||
},
|
||||
|
||||
show_tours: function(tour_id) {
|
||||
@@ -109,6 +112,10 @@ export const getAnalysisRouter = Galaxy =>
|
||||
this.page.display(new FormWrapper.View(_.extend(model.get(form_id), { active_tab: "user" })));
|
||||
},
|
||||
|
||||
show_cloud_auth: function() {
|
||||
this._display_vue_helper(CloudAuth);
|
||||
},
|
||||
|
||||
show_visualizations: function(action_id) {
|
||||
var activeTab = action_id == "list_published" ? "shared" : "user";
|
||||
this.page.display(
|
||||
|
||||
@@ -90,6 +90,15 @@ var Model = Backbone.Model.extend({
|
||||
submit_title: "Create a new key",
|
||||
submit_icon: "fa-check"
|
||||
},
|
||||
cloud_auth: {
|
||||
title: _l("Manage Cloud Authorization"),
|
||||
description: _l(
|
||||
"Add or modify the configuration that grants Galaxy to access your cloud-based resources."
|
||||
),
|
||||
icon: "fa-cloud",
|
||||
submit_title: "Create a new key",
|
||||
submit_icon: "fa-check"
|
||||
},
|
||||
toolbox_filters: {
|
||||
title: _l("Manage Toolbox filters"),
|
||||
description: _l("Customize your Toolbox by displaying or omitting sets of Tools."),
|
||||
@@ -174,6 +183,7 @@ var View = Backbone.View.extend({
|
||||
self._addLink("permissions");
|
||||
self._addLink("make_data_private");
|
||||
self._addLink("api_key");
|
||||
self._addLink("cloud_auth");
|
||||
if (config.enable_openid) {
|
||||
self._addLink("genomespace");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Object.assign, but props transferred must exist in target
|
||||
|
||||
export function safeAssign(target, source = {}) {
|
||||
if (!(source instanceof Object)) {
|
||||
console.warn("safeAssign expected an object, instead got:", source);
|
||||
source = {};
|
||||
}
|
||||
Object.keys(source)
|
||||
.filter(prop => target.hasOwnProperty(prop))
|
||||
.forEach(prop => (target[prop] = source[prop]));
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
$fa-font-path: "~font-awesome/fonts/";
|
||||
@import "~font-awesome/scss/font-awesome.scss";
|
||||
@import "~font-awesome/scss/_mixins";
|
||||
|
||||
// Utility mixin expands to container edges
|
||||
@mixin fill() {
|
||||
position: relative;
|
||||
@@ -29,3 +33,51 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin scrollMe() {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// top selector fixed, bottom list scrolls
|
||||
@mixin scrollingListLayout($fixedTop, $listRegion) {
|
||||
@include fill();
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
#{$fixedTop} {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
#{$listRegion} {
|
||||
flex-grow: 1;
|
||||
@include scrollMe();
|
||||
}
|
||||
}
|
||||
|
||||
// Removes browser formatting of lists
|
||||
|
||||
@mixin list_reset() {
|
||||
list-style: none;
|
||||
padding: 0 0 0 0;
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
// font awesome as a mixin
|
||||
// icon variables come from ~font-awesome/scss/_variables.scss
|
||||
|
||||
@mixin fontawesome($icon) {
|
||||
@include fa-icon();
|
||||
&:before {
|
||||
content: $icon;
|
||||
}
|
||||
span {
|
||||
// Span for text readers
|
||||
// <a><span>Label</span></a>
|
||||
@include sr-only();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
"bootstrap-vue": "^2.0.0-rc.16",
|
||||
"d3": "3",
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"flush-promises": "^1.0.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"handsontable": "^2.0.0",
|
||||
"imask": "^4.1.5",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jquery": "2",
|
||||
"jquery-migrate": "~1.4",
|
||||
|
||||
@@ -162,7 +162,10 @@ let buildconfig = {
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
sourceMap: true,
|
||||
includePaths: ["galaxy/style/scss"]
|
||||
includePaths: [
|
||||
"galaxy/style/scss",
|
||||
path.resolve(__dirname, './node_modules')
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5530,6 +5530,11 @@ flatted@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
|
||||
integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
|
||||
|
||||
flush-promises@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/flush-promises/-/flush-promises-1.0.2.tgz#4948fd58f15281fed79cbafc86293d5bb09b2ced"
|
||||
integrity sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
|
||||
@@ -6480,6 +6485,11 @@ ignore@^4.0.6:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
imask@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/imask/-/imask-4.1.5.tgz#9942993ece68b7b33d03e12781a7401b1207e52c"
|
||||
integrity sha512-n9e3KkJxe6YZKueT02I8/tXpC0vzxmSzoBB5BPi6OPq0rp7qOZfXN5777Dkw6WEYRswlsxm+7pl7WocsG0LWyA==
|
||||
|
||||
immer@1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
||||
|
||||
@@ -44,7 +44,8 @@ class CloudAuthzsSerializer(base.ModelSerializer):
|
||||
'authn_id',
|
||||
'last_update',
|
||||
'last_activity',
|
||||
'create_time'
|
||||
'create_time',
|
||||
'description'
|
||||
])
|
||||
|
||||
def add_serializers(self):
|
||||
@@ -63,7 +64,8 @@ class CloudAuthzsSerializer(base.ModelSerializer):
|
||||
'authn_id' : lambda i, k, **c: self.app.security.encode_id(i.authn_id),
|
||||
'last_update' : lambda i, k, **c: str(i.last_update),
|
||||
'last_activity': lambda i, k, **c: str(i.last_activity),
|
||||
'create_time' : lambda i, k, **c: str(i.create_time)
|
||||
'create_time' : lambda i, k, **c: str(i.create_time),
|
||||
'description' : lambda i, k, **c: str(i.description)
|
||||
})
|
||||
|
||||
|
||||
@@ -80,7 +82,7 @@ class CloudAuthzsDeserializer(base.ModelDeserializer):
|
||||
'authn_id': self.deserialize_and_validate_authn_id,
|
||||
'provider': self.default_deserializer,
|
||||
'config': self.default_deserializer,
|
||||
'deleted': self.default_deserializer
|
||||
'description': self.default_deserializer
|
||||
})
|
||||
|
||||
def deserialize_and_validate_authn_id(self, item, key, val, **context):
|
||||
|
||||
Reference in New Issue
Block a user