mirror of https://github.com/rancher/dashboard.git
569 lines
15 KiB
JavaScript
569 lines
15 KiB
JavaScript
import https from 'https';
|
|
import merge from 'lodash/merge';
|
|
import { SCHEMA } from '@/config/types';
|
|
import { createYaml } from '@/utils/create-yaml';
|
|
import { SPOOFED_API_PREFIX, SPOOFED_PREFIX } from '@/store/type-map';
|
|
import { addParam } from '@/utils/url';
|
|
import { isArray } from '@/utils/array';
|
|
import { deferred } from '@/utils/promise';
|
|
import { streamingSupported, streamJson } from '@/utils/stream';
|
|
import { normalizeType } from './normalize';
|
|
import { classify } from './classify';
|
|
|
|
export const _ALL = 'all';
|
|
export const _MULTI = 'multi';
|
|
export const _ALL_IF_AUTHED = 'allIfAuthed';
|
|
export const _NONE = 'none';
|
|
|
|
export default {
|
|
async request({ state, dispatch, rootGetters }, opt) {
|
|
// Handle spoofed types instead of making an actual request
|
|
// Spoofing is handled here to ensure it's done for both yaml and form editing.
|
|
// It became apparent that this was the only place that both intersected
|
|
if (opt.url.includes(SPOOFED_PREFIX) || opt.url.includes(SPOOFED_API_PREFIX)) {
|
|
const [empty, scheme, type, ...rest] = opt.url.split('/'); // eslint-disable-line no-unused-vars
|
|
const id = rest.join('/'); // Cover case where id contains '/'
|
|
const isApi = scheme === SPOOFED_API_PREFIX;
|
|
const typemapGetter = id ? 'getSpoofedInstance' : 'getSpoofedInstances';
|
|
|
|
const schemas = rootGetters['cluster/all'](SCHEMA);
|
|
// getters return async getSpoofedInstance/getSpoofedInstances fn
|
|
const instance = await rootGetters[`type-map/${ typemapGetter }`](type, id);
|
|
const data = isApi ? createYaml(schemas, type, instance) : instance;
|
|
|
|
return id && !isApi ? data : { data };
|
|
}
|
|
|
|
opt.depaginate = opt.depaginate !== false;
|
|
opt.url = opt.url.replace(/\/*$/g, '');
|
|
opt.httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
|
|
const method = (opt.method || 'get').toLowerCase();
|
|
const headers = (opt.headers || {});
|
|
const key = JSON.stringify(headers) + method + opt.url;
|
|
let waiting;
|
|
|
|
if ( (method === 'get') ) {
|
|
waiting = state.deferredRequests[key];
|
|
|
|
if ( waiting ) {
|
|
const later = deferred();
|
|
|
|
waiting.push(later);
|
|
|
|
// console.log('Deferred request for', key, waiting.length);
|
|
|
|
return later.promise;
|
|
} else {
|
|
// Set it to something so that future requests know to defer.
|
|
waiting = [];
|
|
state.deferredRequests[key] = waiting;
|
|
}
|
|
}
|
|
|
|
if ( opt.stream && state.allowStreaming && state.config.supportsStream && streamingSupported() ) {
|
|
// console.log('Using Streaming for', opt.url);
|
|
|
|
return streamJson(opt.url, opt, opt.onData).then(() => {
|
|
return { finishDeferred: finishDeferred.bind(null, key, 'resolve') };
|
|
}).catch((err) => {
|
|
return onError(err);
|
|
});
|
|
} else {
|
|
// console.log('NOT Using Streaming for', opt.url);
|
|
}
|
|
|
|
return this.$axios(opt).then((res) => {
|
|
if ( opt.depaginate ) {
|
|
// @TODO but API never sends it
|
|
/*
|
|
return new Promise((resolve, reject) => {
|
|
const next = res.pagination.next;
|
|
if (!next ) [
|
|
return resolve();
|
|
}
|
|
|
|
dispatch('request')
|
|
});
|
|
*/
|
|
}
|
|
|
|
let out;
|
|
|
|
if ( opt.responseType ) {
|
|
out = res;
|
|
} else {
|
|
out = responseObject(res);
|
|
}
|
|
|
|
finishDeferred(key, 'resolve', out);
|
|
|
|
return out;
|
|
}).catch(onError);
|
|
|
|
function finishDeferred(key, action = 'resolve', res) {
|
|
const waiting = state.deferredRequests[key] || [];
|
|
|
|
// console.log('Resolving deferred for', key, waiting.length);
|
|
|
|
while ( waiting.length ) {
|
|
waiting.pop()[action](res);
|
|
}
|
|
|
|
delete state.deferredRequests[key];
|
|
}
|
|
|
|
function responseObject(res) {
|
|
let out = res.data;
|
|
|
|
const fromHeader = res.headers['x-api-cattle-auth'];
|
|
|
|
if ( fromHeader && fromHeader !== rootGetters['auth/fromHeader'] ) {
|
|
dispatch('auth/gotHeader', fromHeader, { root: true });
|
|
}
|
|
|
|
if ( res.status === 204 || out === null ) {
|
|
out = {};
|
|
}
|
|
|
|
if ( typeof out !== 'object' ) {
|
|
out = { data: out };
|
|
}
|
|
|
|
Object.defineProperties(out, {
|
|
_status: { value: res.status },
|
|
_statusText: { value: res.statusText },
|
|
_headers: { value: res.headers },
|
|
_req: { value: res.request },
|
|
_url: { value: opt.url },
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
function onError(err) {
|
|
let out = err;
|
|
|
|
if ( err?.response ) {
|
|
const res = err.response;
|
|
|
|
// Go to the logout page for 401s, unless redirectUnauthorized specifically disables (for the login page)
|
|
if ( opt.redirectUnauthorized !== false && process.client && res.status === 401 ) {
|
|
dispatch('auth/logout', opt.logoutOnError, { root: true });
|
|
}
|
|
|
|
if ( typeof res.data !== 'undefined' ) {
|
|
out = responseObject(res);
|
|
}
|
|
}
|
|
|
|
finishDeferred(key, 'reject', out);
|
|
|
|
return Promise.reject(out);
|
|
}
|
|
},
|
|
|
|
async loadSchemas(ctx, watch = true) {
|
|
const {
|
|
getters, dispatch, commit, rootGetters
|
|
} = ctx;
|
|
const res = await dispatch('findAll', { type: SCHEMA, opt: { url: 'schemas', load: false } });
|
|
const spoofedTypes = rootGetters['type-map/allSpoofedSchemas'] ;
|
|
|
|
if (isArray(res.data)) {
|
|
res.data = res.data.concat(spoofedTypes);
|
|
} else if (isArray(res)) {
|
|
res.data = res.concat(spoofedTypes);
|
|
}
|
|
|
|
res.data.forEach((schema) => {
|
|
schema._id = normalizeType(schema.id);
|
|
schema._group = normalizeType(schema.attributes?.group);
|
|
});
|
|
|
|
commit('loadAll', {
|
|
ctx,
|
|
type: SCHEMA,
|
|
data: res.data
|
|
});
|
|
|
|
if ( watch !== false ) {
|
|
dispatch('watch', {
|
|
type: SCHEMA,
|
|
revision: res.revision
|
|
});
|
|
}
|
|
|
|
const all = getters.all(SCHEMA);
|
|
|
|
return all;
|
|
},
|
|
|
|
async findAll(ctx, { type, opt }) {
|
|
const {
|
|
getters, commit, dispatch, rootGetters
|
|
} = ctx;
|
|
|
|
opt = opt || {};
|
|
type = getters.normalizeType(type);
|
|
|
|
if ( !getters.typeRegistered(type) ) {
|
|
commit('registerType', type);
|
|
}
|
|
|
|
if ( opt.force !== true && getters['haveAll'](type) ) {
|
|
return getters.all(type);
|
|
}
|
|
|
|
let load = (opt.load === undefined ? _ALL : opt.load);
|
|
|
|
if ( opt.load === false || opt.load === _NONE ) {
|
|
load = _NONE;
|
|
} else if ( opt.load === _ALL_IF_AUTHED ) {
|
|
const header = rootGetters['auth/fromHeader'];
|
|
|
|
if ( `${ header }` === 'true' || `${ header }` === 'none' ) {
|
|
load = _ALL;
|
|
} else {
|
|
load = _MULTI;
|
|
}
|
|
}
|
|
|
|
console.log(`Find All: [${ ctx.state.config.namespace }] ${ type }`); // eslint-disable-line no-console
|
|
opt = opt || {};
|
|
opt.url = getters.urlFor(type, null, opt);
|
|
opt.stream = opt.stream !== false && load !== _NONE;
|
|
let streamStarted = false;
|
|
let out;
|
|
|
|
let queue = [];
|
|
let streamCollection;
|
|
|
|
opt.onData = function(data) {
|
|
if ( streamStarted ) {
|
|
// Batch loads into groups of 10 to reduce vuex overhead
|
|
queue.push(data);
|
|
|
|
if ( queue.length > 10 ) {
|
|
const tmp = queue;
|
|
|
|
queue = [];
|
|
commit('loadMulti', { ctx, data: tmp });
|
|
}
|
|
} else {
|
|
// The first line is the collection object (sans `data`)
|
|
commit('forgetAll', { type });
|
|
streamStarted = true;
|
|
streamCollection = data;
|
|
}
|
|
};
|
|
|
|
try {
|
|
const res = await dispatch('request', opt);
|
|
|
|
if ( streamStarted ) {
|
|
// Flush any remaining entries left over that didn't get loaded by onData
|
|
if ( queue.length ) {
|
|
commit('loadMulti', { ctx, data: queue });
|
|
queue = [];
|
|
}
|
|
commit('loadedAll', { type });
|
|
const all = getters.all(type);
|
|
|
|
res.finishDeferred(all);
|
|
out = streamCollection;
|
|
} else {
|
|
out = res;
|
|
}
|
|
} catch (e) {
|
|
return Promise.reject(e);
|
|
}
|
|
|
|
if ( load === _NONE ) {
|
|
return out;
|
|
} else if ( out.data ) {
|
|
if ( load === _MULTI ) {
|
|
// This has the effect of adding the response to the store,
|
|
// without replacing all the existing content for that type,
|
|
// and without marking that type as having 'all 'loaded.
|
|
//
|
|
// This is used e.g. to load a partial list of settings before login
|
|
// while still knowing we need to load the full list later.
|
|
commit('loadMulti', {
|
|
ctx,
|
|
data: out.data
|
|
});
|
|
} else {
|
|
commit('loadAll', {
|
|
ctx,
|
|
type,
|
|
data: out.data
|
|
});
|
|
}
|
|
}
|
|
|
|
if ( opt.watch !== false ) {
|
|
dispatch('watch', {
|
|
type,
|
|
revision: out.revision,
|
|
namespace: opt.watchNamespace
|
|
});
|
|
}
|
|
|
|
const all = getters.all(type);
|
|
|
|
return all;
|
|
},
|
|
|
|
async findMatching(ctx, { type, selector, opt }) {
|
|
const { getters, commit, dispatch } = ctx;
|
|
|
|
opt = opt || {};
|
|
console.log(`Find Matching: [${ ctx.state.config.namespace }] ${ type }`, selector); // eslint-disable-line no-console
|
|
type = getters.normalizeType(type);
|
|
|
|
if ( !getters.typeRegistered(type) ) {
|
|
commit('registerType', type);
|
|
}
|
|
if ( opt.force !== true && getters['haveSelector'](type, selector) ) {
|
|
return getters.matching( type, selector );
|
|
}
|
|
|
|
opt = opt || {};
|
|
|
|
opt.filter = opt.filter || {};
|
|
opt.filter['labelSelector'] = selector;
|
|
|
|
opt.url = getters.urlFor(type, null, opt);
|
|
|
|
const res = await dispatch('request', opt);
|
|
|
|
if ( opt.load === false ) {
|
|
return res.data;
|
|
}
|
|
|
|
commit('loadSelector', {
|
|
ctx,
|
|
type,
|
|
entries: res.data,
|
|
selector
|
|
});
|
|
|
|
if ( opt.watch !== false ) {
|
|
dispatch('watch', {
|
|
type,
|
|
selector,
|
|
revision: res.revision
|
|
});
|
|
}
|
|
|
|
return getters.matching( type, selector );
|
|
},
|
|
|
|
// opt:
|
|
// filter: Filter by fields, e.g. {field: value, anotherField: anotherValue} (default: none)
|
|
// limit: Number of records to return per page (default: 1000)
|
|
// sortBy: Sort by field
|
|
// sortOrder: asc or desc
|
|
// url: Use this specific URL instead of looking up the URL for the type/id. This should only be used for bootstrapping schemas on startup.
|
|
// @TODO depaginate: If the response is paginated, retrieve all the pages. (default: true)
|
|
async find(ctx, { type, id, opt }) {
|
|
const { getters, dispatch } = ctx;
|
|
|
|
opt = opt || {};
|
|
|
|
type = normalizeType(type);
|
|
|
|
console.log(`Find: [${ ctx.state.config.namespace }] ${ type } ${ id }`); // eslint-disable-line no-console
|
|
let out;
|
|
|
|
if ( opt.force !== true ) {
|
|
out = getters.byId(type, id);
|
|
|
|
if ( out ) {
|
|
return out;
|
|
}
|
|
}
|
|
|
|
opt = opt || {};
|
|
opt.url = getters.urlFor(type, id, opt);
|
|
|
|
const res = await dispatch('request', opt);
|
|
|
|
await dispatch('load', { data: res });
|
|
|
|
if ( opt.watch !== false ) {
|
|
const watchMsg = {
|
|
type,
|
|
id,
|
|
revision: res?.metadata?.resourceVersion,
|
|
force: opt.forceWatch === true,
|
|
};
|
|
|
|
const idx = id.indexOf('/');
|
|
|
|
if ( idx > 0 ) {
|
|
watchMsg.namespace = id.substr(0, idx);
|
|
watchMsg.id = id.substr(idx + 1);
|
|
}
|
|
|
|
dispatch('watch', watchMsg);
|
|
}
|
|
|
|
out = getters.byId(type, id);
|
|
|
|
return out;
|
|
},
|
|
|
|
load(ctx, { data, existing }) {
|
|
const { getters, commit } = ctx;
|
|
|
|
let type = normalizeType(data.type);
|
|
|
|
if ( !getters.typeRegistered(type) ) {
|
|
commit('registerType', type);
|
|
}
|
|
|
|
if ( data.baseType && data.baseType !== data.type ) {
|
|
type = normalizeType(data.baseType);
|
|
|
|
if ( !getters.typeRegistered(type) ) {
|
|
commit('registerType', type);
|
|
}
|
|
}
|
|
|
|
const id = data?.id || existing?.id;
|
|
|
|
if ( !id ) {
|
|
console.warn('Attempting to load a resource with no id', data, existing); // eslint-disable-line no-console
|
|
|
|
return;
|
|
}
|
|
|
|
commit('load', {
|
|
ctx,
|
|
data,
|
|
existing
|
|
});
|
|
|
|
if ( type === SCHEMA ) {
|
|
commit('type-map/schemaChanged', null, { root: true });
|
|
}
|
|
|
|
return getters['byId'](type, id);
|
|
},
|
|
|
|
loadMulti(ctx, data) {
|
|
const { commit } = ctx;
|
|
|
|
commit('loadMulti', {
|
|
data,
|
|
ctx,
|
|
});
|
|
},
|
|
|
|
loadAll(ctx, { type, data }) {
|
|
const { commit } = ctx;
|
|
|
|
commit('loadAll', {
|
|
ctx,
|
|
type,
|
|
data
|
|
});
|
|
},
|
|
|
|
create(ctx, data) {
|
|
return classify(ctx, data);
|
|
},
|
|
|
|
createPopulated(ctx, userData) {
|
|
const data = ctx.getters['defaultFor'](userData.type);
|
|
|
|
merge(data, userData);
|
|
|
|
return classify(ctx, data);
|
|
},
|
|
|
|
clone(ctx, { resource } = {}) {
|
|
return classify(ctx, resource.toJSON(), true);
|
|
},
|
|
|
|
promptMove({ commit, state }, resources) {
|
|
commit('action-menu/togglePromptMove', resources, { root: true });
|
|
},
|
|
|
|
promptRemove({ commit, state }, resources ) {
|
|
commit('action-menu/togglePromptRemove', resources, { root: true });
|
|
},
|
|
|
|
promptRestore({ commit, state }, resources ) {
|
|
commit('action-menu/togglePromptRestore', resources, { root: true });
|
|
},
|
|
|
|
assignTo({ commit, state }, resources = []) {
|
|
commit('action-menu/toggleAssignTo', resources, { root: true });
|
|
},
|
|
|
|
promptModal({ commit, state }, data ) {
|
|
commit('action-menu/togglePromptModal', data, { root: true });
|
|
},
|
|
|
|
async resourceAction({ getters, dispatch }, {
|
|
resource, actionName, body, opt,
|
|
}) {
|
|
opt = opt || {};
|
|
|
|
if ( !opt.url ) {
|
|
opt.url = resource.actionLinkFor(actionName);
|
|
// opt.url = (resource.actions || resource.actionLinks)[actionName];
|
|
}
|
|
|
|
opt.method = 'post';
|
|
opt.data = body;
|
|
|
|
const res = await dispatch('request', opt);
|
|
|
|
if ( opt.load !== false && res.type === 'collection' ) {
|
|
await dispatch('loadMulti', res.data);
|
|
|
|
return res.data.map(x => getters.byId(x.type, x.id) || x);
|
|
} else if ( opt.load !== false && res.type && res.id ) {
|
|
return dispatch('load', { data: res });
|
|
} else {
|
|
return res;
|
|
}
|
|
},
|
|
|
|
promptUpdate({ commit, state }, resources = []) {
|
|
commit('action-menu/togglePromptUpdate', resources, { root: true });
|
|
},
|
|
|
|
async collectionAction({ getters, dispatch }, {
|
|
type, actionName, body, opt
|
|
}) {
|
|
opt = opt || {};
|
|
|
|
if ( !opt.url ) {
|
|
// Cheating, but cheaper than loading the whole collection...
|
|
const schema = getters['schemaFor'](type);
|
|
|
|
opt.url = addParam(schema.links.collection, 'action', actionName);
|
|
}
|
|
|
|
opt.method = 'post';
|
|
opt.data = body;
|
|
|
|
const res = await dispatch('request', opt);
|
|
|
|
if ( opt.load !== false && res.type === 'collection' ) {
|
|
await dispatch('loadMulti', res.data);
|
|
|
|
return res.data.map(x => getters.byId(x.type, x.id) || x);
|
|
} else if ( opt.load !== false && res.type && res.id ) {
|
|
return dispatch('load', { data: res });
|
|
} else {
|
|
return res;
|
|
}
|
|
},
|
|
};
|