mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Cloud auth key UI elements
This commit is contained in:
@@ -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) {
|
||||
let result = this.stringifyValue(value);
|
||||
if (this.mask.length) {
|
||||
return this.masker.resolve(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@import "scss/mixins";
|
||||
@import "./operations";
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// menu
|
||||
.operations {
|
||||
.help a {
|
||||
@include fontawesome($fa-var-info-circle);
|
||||
}
|
||||
.filter a {
|
||||
@include fontawesome($fa-var-filter);
|
||||
}
|
||||
.create a {
|
||||
@include fontawesome($fa-var-plus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Transitions
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity .5s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
// Unhose base.scss hacks
|
||||
|
||||
.cloud-auth div.form-row label {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#deleteCredentialModal {
|
||||
.modal-body {
|
||||
display: none;
|
||||
}
|
||||
.modal-dialog {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/* global expect */
|
||||
import sinon from "sinon";
|
||||
|
||||
import { default as CloudAuth, __RewireAPI__ as rewire } from "./CloudAuth";
|
||||
import CloudAuthItem from "./CloudAuthItem.vue";
|
||||
|
||||
import { mount, shallowMount, createLocalVue } from "@vue/test-utils";
|
||||
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 wrapper, stub;
|
||||
|
||||
let mockSvc = {
|
||||
listCredentials: async () => null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(CloudAuth, { localVue });
|
||||
rewire.__Rewire__("svc", mockSvc);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (stub) {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
describe("initialization", () => {
|
||||
|
||||
// fake list load
|
||||
stub = sinon.stub(mockSvc, "listCredentials").resolves(listCredentials);
|
||||
|
||||
it("should mount", () => {
|
||||
assert(wrapper);
|
||||
})
|
||||
|
||||
it("should show one item", () => {
|
||||
let keyItems = wrapper.findAll(".cloud-auth-key");
|
||||
let firstitem = keyItems.at(0);
|
||||
assert(keyItems);
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<section class="cloud-auth pt-1">
|
||||
<header>
|
||||
|
||||
<hgroup class="cloud-auth-title">
|
||||
<h1>Manage Cloud Authorization</h1>
|
||||
<nav class="operations">
|
||||
<ul>
|
||||
<li class="help">
|
||||
<a @click.prevent="showHelp = !showHelp"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Instructions"
|
||||
title="Instructions">
|
||||
<span>Instructions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="filter">
|
||||
<a @click.prevent="showFilter = !showFilter"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Filter Results"
|
||||
title="Filter Results">
|
||||
<span>Filter</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="create">
|
||||
<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 variant="success" @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="onExpand(credential, $event)" />
|
||||
</transition-group>
|
||||
|
||||
</b-list-group>
|
||||
</div>
|
||||
|
||||
<b-modal id="deleteCredentialModal" ref="deleteModal"
|
||||
title="Delete Item?" size="sm"
|
||||
@ok="onConfirmDelete"
|
||||
@cancel="onCancelDelete">
|
||||
</b-modal>
|
||||
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import CloudAuthItem from "./CloudAuthItem";
|
||||
import { Credential } from "./model";
|
||||
import svc from "./model/service";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CloudAuthItem
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
filter: "",
|
||||
showHelp: true,
|
||||
showFilter: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredItems() {
|
||||
return this.items
|
||||
.filter(o => o.filter(this.filter));
|
||||
},
|
||||
filterDescription() {
|
||||
return `${this.filteredItems.length} matches out of ${this.items.length} items`;
|
||||
},
|
||||
deleteButtonVariant() {
|
||||
return this.showDeleted ? "primary": "secondary";
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showDeleted(deleted) {
|
||||
this.loadCredentials({ deleted });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCreate() {
|
||||
this.addItem(Credential.create());
|
||||
},
|
||||
onSave(item) {
|
||||
if (!item.valid) {
|
||||
return;
|
||||
}
|
||||
item.loading = true;
|
||||
svc.saveCredential(item)
|
||||
.then(result => {
|
||||
item.id = result.id;
|
||||
item.updateState();
|
||||
})
|
||||
.catch(err => console.warn('bad save', err))
|
||||
.finally(() => item.loading = false);
|
||||
},
|
||||
onDelete(doomed) {
|
||||
// bootstrapVue modal v-model property is bugged
|
||||
// so need to set and retrieve the doomed item manually
|
||||
this.doomedItem = doomed;
|
||||
this.$refs.deleteModal.show();
|
||||
},
|
||||
onConfirmDelete() {
|
||||
|
||||
let doomed = this.doomedItem;
|
||||
doomed.loading = true;
|
||||
|
||||
svc.deleteCredential(doomed)
|
||||
.then(() => this.removeItem(doomed))
|
||||
.catch(err => console.warn('bad delete', err))
|
||||
.finally(() => {
|
||||
doomed.loading = false;
|
||||
this.onCancelDelete();
|
||||
});
|
||||
},
|
||||
onCancelDelete() {
|
||||
this.doomedItem = null;
|
||||
},
|
||||
onExpand(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);
|
||||
},
|
||||
loadCredentials(params = {}) {
|
||||
this.loading = true;
|
||||
svc.listCredentials(params)
|
||||
.then(items => this.items = items)
|
||||
.catch(err => console.warn("bad load", err))
|
||||
.finally(() => this.loading = false);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" src="./CloudAuth.scss"></style>
|
||||
@@ -0,0 +1,70 @@
|
||||
@import "scss/theme/blue.scss";
|
||||
@import "scss/mixins";
|
||||
@import "./operations";
|
||||
|
||||
|
||||
.cloud-auth-key {
|
||||
|
||||
header hgroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h4 {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
.custom-select {
|
||||
background: none; // removes wierd double arrows on select
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// save disk changes color based on form valid/dirty
|
||||
|
||||
&.valid .save a {
|
||||
color: $state-success-text;
|
||||
}
|
||||
&:not(.valid) .save a {
|
||||
color: $state-danger-text;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<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</span>
|
||||
</a>
|
||||
</li>
|
||||
</transition>
|
||||
<li class="delete">
|
||||
<a @click.prevent="$emit('delete', credential)"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Delete this key"
|
||||
title="Delete this key">
|
||||
<span>Delete</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="mt-4 pt-4 border-top"
|
||||
v-model="credential"
|
||||
@click.self="expand()"
|
||||
@save="$emit('save', credential)"
|
||||
@delete="$emit('delete', credential)" />
|
||||
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Vue from "vue";
|
||||
import { Credential } from "./model";
|
||||
import CredentialForm from "./CredentialForm";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CredentialForm
|
||||
},
|
||||
props: {
|
||||
credential: { type: Credential, required: true }
|
||||
},
|
||||
computed: {
|
||||
statusClasses() {
|
||||
let { expanded, valid, dirty, loading } = this.credential;
|
||||
return { loading, expanded, valid, dirty, collapsed: !expanded };
|
||||
},
|
||||
variant() {
|
||||
return this.expanded ? "" : "primary";
|
||||
},
|
||||
expanded() {
|
||||
return this.credential.expanded;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
expand(forceState) {
|
||||
let expanded = (forceState !== undefined) ? forceState : !this.expanded;
|
||||
this.$emit("expand", { expanded });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" src="./CloudAuthItem.scss"></style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<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"
|
||||
: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,103 @@
|
||||
<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>
|
||||
<b-button type="cancel"
|
||||
@click.prevent="$emit('delete', credential)">
|
||||
Delete Key
|
||||
</b-button>
|
||||
<b-button type="submit"
|
||||
aria-label="Save Key"
|
||||
@click.prevent="$emit('save', credential)">
|
||||
Save This Key
|
||||
</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() {
|
||||
let opts = Array.from(ResourceProviders.entries())
|
||||
.map(([value, o]) => ({ value, text: o.label }));
|
||||
return opts;
|
||||
},
|
||||
config() {
|
||||
return this.credential.config;
|
||||
}
|
||||
},
|
||||
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) {
|
||||
let 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,60 @@
|
||||
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) {
|
||||
let 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,21 @@
|
||||
import { AzureConfig } from "./AzureConfig";
|
||||
|
||||
describe("AzureConfig", () => {
|
||||
|
||||
it("should instantiate", () => {
|
||||
let instance = new AzureConfig();
|
||||
assert(instance);
|
||||
assert(!instance.dirty);
|
||||
assert(!instance.valid);
|
||||
})
|
||||
|
||||
it("should validate client_secret", () => {
|
||||
let instance = new AzureConfig();
|
||||
instance.tenant_id = "abc";
|
||||
instance.client_secret = "abc";
|
||||
assert(instance.fieldValid("client_secret"));
|
||||
assert(!instance.fieldValid("tenant_id"));
|
||||
assert(!instance.fieldValid("client_id"));
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
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
|
||||
let 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() {
|
||||
let 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) {
|
||||
|
||||
let klass = this; // this will be a class
|
||||
if (!transients.has(klass)) {
|
||||
transients.set(klass, new Set());
|
||||
}
|
||||
|
||||
let 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)) {
|
||||
let validator = validators.get(this);
|
||||
return validator(model);
|
||||
}
|
||||
throw new Error("Missing validator");
|
||||
}
|
||||
|
||||
static setValidator(validationFunction) {
|
||||
validators.set(this, validationFunction);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
let 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
|
||||
|
||||
filter(searchText = "") {
|
||||
// TODO: better text filtering
|
||||
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) {
|
||||
|
||||
let 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,114 @@
|
||||
import { Credential } from "./Credential";
|
||||
import { ResourceProviders } from "./ResourceProviders";
|
||||
|
||||
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,30 @@
|
||||
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() {
|
||||
let url = getUrl("api/cloud/authz");
|
||||
let response = await axios.get(url);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Bad mojo");
|
||||
}
|
||||
return response.data.map(Credential.create);
|
||||
}
|
||||
|
||||
export async function getCredential(id) {
|
||||
let url = getUrl("api/cloud/authz/${id}");
|
||||
let response = await axios.get(url);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Bad load");
|
||||
}
|
||||
return Credential.create(response.data);
|
||||
}
|
||||
|
||||
export async function saveCredential(newItem) {
|
||||
let model = Credential.create(newItem);
|
||||
let response = await saveOrUpdate(model);
|
||||
if (response.status != 200) {
|
||||
throw new Error("bad save");
|
||||
}
|
||||
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) {
|
||||
let model = Credential.create(doomed);
|
||||
if (model.id) {
|
||||
let url = getUrl(`api/cloud/authz/${doomed.id}`);
|
||||
let response = await axios.delete(url);
|
||||
if (response.status != 200) {
|
||||
throw new Error("Bad delete");
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// Memoize results (basically never changes)
|
||||
|
||||
let identityProviders;
|
||||
|
||||
export async function getIdentityProviders() {
|
||||
if (!identityProviders) {
|
||||
let url = getUrl("authnz");
|
||||
let 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
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
I put the styles for this little icon menu in its own file because
|
||||
I think this should probably become its own general component
|
||||
|
||||
TODO: Build operations menu component for reuse wherever we do little
|
||||
icon doodads
|
||||
*/
|
||||
|
||||
// TODO: Show this to Sam, this is what I need to import
|
||||
// to use ml-2 because our own base.scss is not importable
|
||||
@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";
|
||||
|
||||
|
||||
.operations {
|
||||
margin-bottom: 0;
|
||||
|
||||
ul {
|
||||
@include list_reset();
|
||||
display: flex;
|
||||
li:not(:first-child) {
|
||||
@extend .ml-2; // terrible
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"authn_id": "f2db41e1fa331b3e",
|
||||
"user_id": "f2db41e1fa331b3e",
|
||||
"deleted": "True",
|
||||
"last_update": "2019-03-27 10:08:27.711092",
|
||||
"last_activity": "2019-03-27 10:08:27.711104",
|
||||
"create_time": "2019-03-27 17:08:27.712045",
|
||||
"provider": "aws",
|
||||
"model_class": "CloudAuthz",
|
||||
"config": {
|
||||
"role_arn": "arn:sws:idm::347162595"
|
||||
},
|
||||
"description": "floob",
|
||||
"id": "a799d38679e985db"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
<section class="cloud-auth"><header><hgroup class="cloud-auth-title"><h2>Cloud
|
||||
Auth Keys</h2> <nav class="operations"><ul><li><a class="fa
|
||||
fa-info-circle"><span>Help</span></a></li> <li><a
|
||||
class="fa fa-filter"><span>Filter</span></a></li>
|
||||
<li><a class="fa fa-plus"><span>New Item</span></a></li></ul></nav></hgroup>
|
||||
<hgroup class="help active"><p>Lorem ipsum dolor sit amet, consectetur
|
||||
adipiscing elit.
|
||||
Fusce vehicula placerat ante et tincidunt. Donec congue
|
||||
pellentesque tortor sagittis feugiat. Duis rhoncus arcu
|
||||
id erat ultrices egestas. Suspendisse rutrum ipsum id</p></hgroup>
|
||||
<hgroup class="filter active"><fieldset class="form-group"><!----><div
|
||||
tabindex="-1" role="group"><input type="text"
|
||||
placeholder="Filter" class="form-control"><!----><!----><small
|
||||
tabindex="-1" class="form-text text-muted">0 matches out
|
||||
of 0 items</small></div></fieldset></hgroup></header>
|
||||
<div class="scroll-container"><div class="list-group"><button type="button"
|
||||
class="list-group-item cloud-auth-key list-group-item-success
|
||||
list-group-item-action">
|
||||
Create New Authorization
|
||||
</button> </div></div> <div><div
|
||||
id="deleteCredentialModal___BV_modal_outer_" style="position:
|
||||
absolute; z-index: 2000;"><div id="deleteCredentialModal"
|
||||
role="dialog" tabindex="-1" aria-hidden="true" class="modal
|
||||
fade" style="display: none;"><div class="modal-dialog modal-sm"><div
|
||||
role="document"
|
||||
id="deleteCredentialModal___BV_modal_content_"
|
||||
aria-labelledby="deleteCredentialModal___BV_modal_header_"
|
||||
aria-describedby="deleteCredentialModal___BV_modal_body_"
|
||||
class="modal-content"><header
|
||||
id="deleteCredentialModal___BV_modal_header_"
|
||||
class="modal-header"><h5 class="modal-title">Delete
|
||||
Item?</h5><button type="button"
|
||||
aria-label="Close" class="close">×</button></header><div
|
||||
id="deleteCredentialModal___BV_modal_body_"
|
||||
class="modal-body"></div><footer
|
||||
id="deleteCredentialModal___BV_modal_footer_"
|
||||
class="modal-footer"><button type="button"
|
||||
class="btn btn-secondary">Cancel</button><button
|
||||
type="button" class="btn btn-primary">OK</button></footer></div></div></div><!----><!----></div></div></section>
|
||||
@@ -0,0 +1,31 @@
|
||||
/authnz
|
||||
|
||||
// options for authn dropdown
|
||||
|
||||
[
|
||||
{"id": "f597429621d6eb2b", "provider": "google-openidconnect"},
|
||||
{"id": "f597429621d6eb2b", "provider": "google-openidconnect"},
|
||||
{"id": "f597429621d6eb2b", "provider": "google-openidconnect"},
|
||||
{"id": "f597429621d6eb2b", "provider": "google-openidconnect"},
|
||||
{"id": "f597429621d6eb2b", "provider": "google-openidconnect"}
|
||||
]
|
||||
|
||||
{
|
||||
"provider": "aws",
|
||||
"authn_id":"f2db41e1fa331b3e",
|
||||
"description": "asdfasdf",
|
||||
"config": {
|
||||
"role_arn": "arn:aws:iam::347162595075:role/CloudAuthzTests"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"provider": "azure",
|
||||
"authn_id": "f2db41e1fa331b3e",
|
||||
"description": "asdfasdf",
|
||||
"config": {
|
||||
'tenant_id': TENANT_ID,
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"authn_id": "f2db41e1fa331b3e",
|
||||
"user_id": "f2db41e1fa331b3e",
|
||||
"last_update": "2019-03-24 23:29:10.939661",
|
||||
"last_activity": "2019-03-24 23:29:10.939670",
|
||||
"provider": "aws",
|
||||
"model_class": "CloudAuthz",
|
||||
"config": {
|
||||
"role_arn": "asdfadfasdf"
|
||||
},
|
||||
"id": "ebfb8f50c6abde6d"
|
||||
},
|
||||
{
|
||||
"authn_id": "f2db41e1fa331b3e",
|
||||
"user_id": "f2db41e1fa331b3e",
|
||||
"last_update": "2019-03-24 23:25:51.678020",
|
||||
"last_activity": "2019-03-24 23:25:51.678030",
|
||||
"provider": "aws",
|
||||
"model_class": "CloudAuthz",
|
||||
"config": {
|
||||
"role_arn": "asdfadfasdf"
|
||||
},
|
||||
"id": "1cd8e2f6b131e891"
|
||||
},
|
||||
{
|
||||
"authn_id": "f2db41e1fa331b3e",
|
||||
"user_id": "f2db41e1fa331b3e",
|
||||
"last_update": "2019-03-23 15:43:14.511896",
|
||||
"last_activity": "2019-03-23 15:43:14.511905",
|
||||
"provider": "aws",
|
||||
"model_class": "CloudAuthz",
|
||||
"config": {
|
||||
"role_arn": "asdfadfasdf"
|
||||
},
|
||||
"id": "f597429621d6eb2b"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
<section class="cloud-auth">
|
||||
<header>
|
||||
<hgroup class="cloud-auth-title">
|
||||
<h2>Cloud Auth Keys</h2>
|
||||
<nav class="operations">
|
||||
<ul><li><a class="fa fa-info-circle"><span>Help</span></a></li>
|
||||
<li><a class="fa fa-filter"><span>Filter</span></a></li>
|
||||
<li><a class="fa fa-plus"><span>New Item</span></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</hgroup>
|
||||
<hgroup class="help active">
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Fusce vehicula placerat ante et tincidunt. Donec congue
|
||||
pellentesque tortor sagittis feugiat. Duis rhoncus arcu
|
||||
id erat ultrices egestas. Suspendisse rutrum ipsum id</p>
|
||||
</hgroup>
|
||||
|
||||
<hgroup class="filter active">
|
||||
<b-form-group-stub description="0 matches out of 0 items">
|
||||
<b-form-input-stub value="" placeholder="Filter"
|
||||
type="text"></b-form-input-stub>
|
||||
</b-form-group-stub>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<div class="scroll-container">
|
||||
<b-list-group-stub tag="div">
|
||||
<b-list-group-item-stub tag="div" button="true" variant="success" target="_self" event="click" routertag="a" class="cloud-auth-key">
|
||||
Create New Authorization
|
||||
</b-list-group-item-stub>
|
||||
</b-list-group-stub>
|
||||
</div>
|
||||
|
||||
<b-modal-stub id="deleteCredentialModal" title="Delete Item?"
|
||||
titletag="h5" size="sm" buttonsize="" headercloselabel="Close"
|
||||
canceltitle="Cancel" oktitle="OK" cancelvariant="secondary"
|
||||
okvariant="primary">
|
||||
</b-modal-stub>
|
||||
|
||||
</section>
|
||||
@@ -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) {
|
||||
let 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 = {}) {
|
||||
let instance = Vue.extend(component);
|
||||
let vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new instance(props).$mount(vm);
|
||||
let container = document.createElement("div");
|
||||
this.page.display(container);
|
||||
let 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,13 @@ 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."),
|
||||
@@ -166,6 +173,7 @@ var View = Backbone.View.extend({
|
||||
self._addLink("permissions");
|
||||
self._addLink("make_data_private");
|
||||
self._addLink("api_key");
|
||||
self._addLink("cloud_auth");
|
||||
if (config.has_user_tool_filters) {
|
||||
self._addLink("toolbox_filters");
|
||||
}
|
||||
|
||||
@@ -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,8 @@
|
||||
$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 +34,53 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"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')
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6501,6 +6501,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"
|
||||
|
||||
Reference in New Issue
Block a user