mirror of https://github.com/rancher/dashboard.git
319 lines
8.5 KiB
Vue
319 lines
8.5 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import day from 'dayjs';
|
|
import sortBy from 'lodash/sortBy';
|
|
import { MANAGEMENT, NORMAN } from '@shell/config/types';
|
|
import { Banner } from '@components/Banner';
|
|
import DetailText from '@shell/components/DetailText';
|
|
import Footer from '@shell/components/form/Footer';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import { RadioGroup } from '@components/Form/Radio';
|
|
import Select from '@shell/components/form/Select';
|
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
import { diffFrom } from '@shell/utils/time';
|
|
import { filterHiddenLocalCluster, filterOnlyKubernetesClusters } from '@shell/utils/cluster';
|
|
import { SETTING } from '@shell/config/settings';
|
|
|
|
export default {
|
|
components: {
|
|
Banner,
|
|
DetailText,
|
|
Footer,
|
|
LabeledInput,
|
|
LabeledSelect,
|
|
RadioGroup,
|
|
Select,
|
|
},
|
|
|
|
mixins: [CreateEditView],
|
|
|
|
data() {
|
|
// Get the setting that defines the max token TTL allowed (in minutes)
|
|
const maxTTLSetting = this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.AUTH_TOKEN_MAX_TTL_MINUTES);
|
|
let maxTTL = 0;
|
|
|
|
try {
|
|
maxTTL = parseInt(maxTTLSetting.value || maxTTLSetting.default, 10);
|
|
} catch (e) {
|
|
maxTTL = 0;
|
|
}
|
|
|
|
return {
|
|
errors: null,
|
|
form: {
|
|
expiryType: 'never',
|
|
customExpiry: 0,
|
|
customExpiryUnits: 'minute',
|
|
},
|
|
created: null,
|
|
ttlLimited: false,
|
|
accessKey: '',
|
|
secretKey: '',
|
|
maxTTL,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters({ t: 'i18n/t' }),
|
|
scopes() {
|
|
const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
|
|
const kubeClusters = filterHiddenLocalCluster(filterOnlyKubernetesClusters(all, this.$store), this.$store);
|
|
let out = kubeClusters.map((opt) => ({ value: opt.id, label: opt.nameDisplay }));
|
|
|
|
out = sortBy(out, ['label']);
|
|
out.unshift( { value: '', label: this.t('accountAndKeys.apiKeys.add.noScope') } );
|
|
|
|
return out;
|
|
},
|
|
|
|
expiryOptions() {
|
|
const options = ['never', 'day', 'month', 'year', 'custom'];
|
|
let opts = options.map((opt) => ({ value: opt, label: this.t(`accountAndKeys.apiKeys.add.expiry.options.${ opt }`) }));
|
|
|
|
// When the TTL is anything other than 0, present only two options
|
|
// (1) The maximum allowed
|
|
// (2) Custom
|
|
if (this.maxTTL !== 0 ) {
|
|
const now = day();
|
|
const expiry = now.add(this.maxTTL, 'minute');
|
|
const max = diffFrom(expiry, now, this.t);
|
|
|
|
opts = opts.filter((opt) => opt.value === 'custom');
|
|
opts.unshift({ value: 'max', label: this.t('accountAndKeys.apiKeys.add.expiry.options.maximum', { value: max.string }) });
|
|
}
|
|
|
|
return opts;
|
|
},
|
|
expiryUnitsOptions() {
|
|
const options = ['minute', 'hour', 'day', 'month', 'year'];
|
|
const filtered = this.filterOptionsForTTL(options);
|
|
|
|
return filtered.map((opt) => ({ value: opt, label: this.t(`accountAndKeys.apiKeys.add.customExpiry.options.${ opt }`) }));
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
// Auto-select the first option on load
|
|
this.form.expiryType = this.expiryOptions[0].value;
|
|
},
|
|
|
|
methods: {
|
|
filterOptionsForTTL(opts) {
|
|
return opts.filter((option) => {
|
|
if (option === 'never' ) {
|
|
return this.maxTTL === 0;
|
|
} else if (option === 'custom') {
|
|
return true;
|
|
} else {
|
|
const now = day();
|
|
const expiry = day().add(1, option);
|
|
const ttl = expiry.diff(now) / 60000; // Convert to minutes
|
|
|
|
return this.maxTTL === 0 || ttl <= this.maxTTL;
|
|
}
|
|
});
|
|
},
|
|
|
|
async actuallySave(url) {
|
|
this.updateExpiry();
|
|
if ( this.isCreate ) {
|
|
// Description is a bit weird, so need to clone and set this
|
|
// rather than use this.value - need to find a way to set this if we ever
|
|
// want to allow edit (which I don't think we do)
|
|
const res = await this.value.save();
|
|
|
|
this.created = res;
|
|
this.ttlLimited = res.ttl !== this.value.ttl;
|
|
const token = this.created.token.split(':');
|
|
|
|
this.accessKey = token[0];
|
|
this.secretKey = (token.length > 1) ? token[1] : '';
|
|
this.token = this.created.token;
|
|
|
|
// Force a refresh of the token so we get the expiry date correctly
|
|
await this.$store.dispatch('rancher/find', {
|
|
type: NORMAN.TOKEN,
|
|
id: res.id,
|
|
opt: { force: true }
|
|
}, { root: true });
|
|
} else {
|
|
// Note: update of existing key not supported currently
|
|
await this.value.save();
|
|
}
|
|
},
|
|
|
|
done() {
|
|
if (!this.created) {
|
|
this.doneCreate();
|
|
}
|
|
},
|
|
|
|
doneCreate() {
|
|
this.$router.push({ name: 'account' });
|
|
},
|
|
|
|
updateExpiry() {
|
|
const v = this.form.expiryType;
|
|
const increment = (v === 'custom') ? parseInt(this.form.customExpiry) : 1;
|
|
const units = (v === 'custom') ? this.form.customExpiryUnits : v;
|
|
let ttl = 0;
|
|
|
|
if (units === 'max') {
|
|
ttl = this.maxTTL * 60 * 1000;
|
|
} else if ( units !== 'never' ) {
|
|
const now = day();
|
|
const expiry = day().add(increment, units);
|
|
|
|
ttl = expiry.diff(now);
|
|
}
|
|
this.value.ttl = ttl;
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="!created">
|
|
<div class="pl-10 pr-10">
|
|
<LabeledInput
|
|
key="description"
|
|
v-model:value="value.description"
|
|
:placeholder="t('accountAndKeys.apiKeys.add.description.placeholder')"
|
|
label-key="accountAndKeys.apiKeys.add.description.label"
|
|
mode="edit"
|
|
:min-height="30"
|
|
/>
|
|
|
|
<LabeledSelect
|
|
v-model:value="value.clusterId"
|
|
class="mt-20 scope-select"
|
|
label-key="accountAndKeys.apiKeys.add.scope"
|
|
:options="scopes"
|
|
/>
|
|
|
|
<h5 class="pt-20">
|
|
{{ t('accountAndKeys.apiKeys.add.expiry.label') }}
|
|
</h5>
|
|
|
|
<div class="ml-10">
|
|
<RadioGroup
|
|
v-model:value="form.expiryType"
|
|
:options="expiryOptions"
|
|
data-testid="expiry__options"
|
|
class="mr-20"
|
|
name="expiryGroup"
|
|
/>
|
|
<div class="ml-20 mt-10 expiry">
|
|
<input
|
|
v-model="form.customExpiry"
|
|
:disabled="form.expiryType !== 'custom'"
|
|
type="number"
|
|
:mode="mode"
|
|
:aria-label="t('accountAndKeys.apiKeys.add.ariaLabel.expiration')"
|
|
>
|
|
<Select
|
|
v-model:value="form.customExpiryUnits"
|
|
:disabled="form.expiryType !== 'custom'"
|
|
:options="expiryUnitsOptions"
|
|
:clearable="false"
|
|
:reduce="opt=>opt.value"
|
|
:aria-label="t('accountAndKeys.apiKeys.add.ariaLabel.expirationUnits')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Footer
|
|
:errors="errors"
|
|
:mode="mode"
|
|
@save="save"
|
|
@done="done"
|
|
/>
|
|
</div>
|
|
<div v-else>
|
|
<div>{{ t('accountAndKeys.apiKeys.info.keyCreated') }}</div>
|
|
|
|
<DetailText
|
|
:value="accessKey"
|
|
label-key="accountAndKeys.apiKeys.info.accessKey"
|
|
class="mt-20"
|
|
/>
|
|
<DetailText
|
|
:value="secretKey"
|
|
label-key="accountAndKeys.apiKeys.info.secretKey"
|
|
class="mt-20"
|
|
/>
|
|
|
|
<p class="mt-20">
|
|
{{ t('accountAndKeys.apiKeys.info.bearerTokenTip') }}
|
|
</p>
|
|
|
|
<DetailText
|
|
:value="token"
|
|
label-key="accountAndKeys.apiKeys.info.bearerToken"
|
|
class="mt-20"
|
|
/>
|
|
|
|
<Banner
|
|
color="warning"
|
|
class="mt-20"
|
|
>
|
|
<div>
|
|
{{ t('accountAndKeys.apiKeys.info.saveWarning') }}
|
|
</div>
|
|
</Banner>
|
|
|
|
<Banner
|
|
v-if="ttlLimited"
|
|
color="warning"
|
|
class="mt-20"
|
|
>
|
|
<div>
|
|
{{ t('accountAndKeys.apiKeys.info.ttlLimitedWarning') }}
|
|
</div>
|
|
</Banner>
|
|
|
|
<div class="buttons mt-20">
|
|
<div class="right">
|
|
<button
|
|
type="button"
|
|
data-testid="token_done_create_button"
|
|
class="btn role-primary"
|
|
@click="doneCreate"
|
|
>
|
|
<t k="generic.done" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.scope-select {
|
|
min-width: 33%;
|
|
width: unset;
|
|
}
|
|
.expiry {
|
|
display: flex;
|
|
> * {
|
|
flex: 0 0 200px;
|
|
margin-right: 10px;
|
|
}
|
|
}
|
|
|
|
.buttons {
|
|
display: grid;
|
|
grid-template-areas: "left right";
|
|
grid-template-columns: "min-content auto";
|
|
|
|
.right {
|
|
grid-area: right;
|
|
text-align: right;
|
|
|
|
.btn, button {
|
|
margin: 0 0 0 $column-gutter;
|
|
}
|
|
}
|
|
}
|
|
</style>
|