From eec08338091c46effe55c0c6218ec2dc5cd51a4a Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 12 Jun 2017 14:21:43 -0700 Subject: [PATCH] [uiSettings] add `uiSettingDefaults` export type --- src/core_plugins/kibana/index.js | 5 +- .../kibana/ui_setting_defaults.js} | 43 +---- src/core_plugins/tests_bundle/index.js | 5 +- src/core_plugins/timelion/index.js | 45 +++++- src/server/kbn_server.js | 5 +- src/ui/index.js | 4 +- .../ui_settings_mixin_integration.js | 16 +- .../__tests__/ui_settings_service.js | 149 ++++++++---------- src/ui/ui_settings/ui_exports_consumer.js | 24 +++ src/ui/ui_settings/ui_settings_mixin.js | 23 ++- src/ui/ui_settings/ui_settings_service.js | 10 +- .../ui_settings_service_factory.js | 6 +- .../ui_settings_service_for_request.js | 24 ++- 13 files changed, 210 insertions(+), 149 deletions(-) rename src/{ui/ui_settings/defaults.js => core_plugins/kibana/ui_setting_defaults.js} (89%) create mode 100644 src/ui/ui_settings/ui_exports_consumer.js diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js index 611d7cbd215f..b3fe0bc2dd07 100644 --- a/src/core_plugins/kibana/index.js +++ b/src/core_plugins/kibana/index.js @@ -13,6 +13,7 @@ import { registerSuggestionsApi } from './server/routes/api/suggestions'; import * as systemApi from './server/lib/system_api'; import handleEsError from './server/lib/handle_es_error'; import mappings from './mappings.json'; +import { getUiSettingDefaults } from './ui_setting_defaults'; import { injectVars } from './inject_vars'; @@ -106,7 +107,9 @@ export default function (kibana) { translations: [ resolve(__dirname, './translations/en.json') ], - mappings + + mappings, + uiSettingDefaults: getUiSettingDefaults(), }, preInit: async function (server) { diff --git a/src/ui/ui_settings/defaults.js b/src/core_plugins/kibana/ui_setting_defaults.js similarity index 89% rename from src/ui/ui_settings/defaults.js rename to src/core_plugins/kibana/ui_setting_defaults.js index 10f383a1603d..88ad5cef8446 100644 --- a/src/ui/ui_settings/defaults.js +++ b/src/core_plugins/kibana/ui_setting_defaults.js @@ -1,6 +1,6 @@ import moment from 'moment-timezone'; -export function getDefaultSettings() { +export function getUiSettingDefaults() { const weekdays = moment.weekdays().slice(); const [defaultWeekday] = weekdays; @@ -283,47 +283,6 @@ export function getDefaultSettings() { value: 2000, description: 'The maximum number of buckets a single datasource can return' }, - // Timelion stuff - 'timelion:showTutorial': { - value: false, - description: 'Should I show the tutorial by default when entering the timelion app?' - }, - 'timelion:es.timefield': { - value: '@timestamp', - description: 'Default field containing a timestamp when using .es()' - }, - 'timelion:es.default_index': { - value: '_all', - description: 'Default elasticsearch index to search with .es()' - }, - 'timelion:target_buckets': { - value: 200, - description: 'The number of buckets to shoot for when using auto intervals' - }, - 'timelion:max_buckets': { - value: 2000, - description: 'The maximum number of buckets a single datasource can return' - }, - 'timelion:default_columns': { - value: 2, - description: 'Number of columns on a timelion sheet by default' - }, - 'timelion:default_rows': { - value: 2, - description: 'Number of rows on a timelion sheet by default' - }, - 'timelion:min_interval': { - value: '1ms', - description: 'The smallest interval that will be calculated when using "auto"' - }, - 'timelion:graphite.url': { - value: 'https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', - description: '[experimental] The URL of your graphite host' - }, - 'timelion:quandl.key': { - value: 'someKeyHere', - description: '[experimental] Your API key from www.quandl.com' - }, 'state:storeInSessionStorage': { value: false, description: 'The URL can sometimes grow to be too large for some browsers to ' + diff --git a/src/core_plugins/tests_bundle/index.js b/src/core_plugins/tests_bundle/index.js index e78f4bd008df..6c394b80f2f9 100644 --- a/src/core_plugins/tests_bundle/index.js +++ b/src/core_plugins/tests_bundle/index.js @@ -1,5 +1,4 @@ import { union } from 'lodash'; -import { getDefaultSettings } from '../../ui/ui_settings/defaults'; import findSourceFiles from './find_source_files'; import { fromRoot } from '../../utils'; @@ -57,7 +56,9 @@ export default (kibana) => { }); } - env.defaultUiSettings = getDefaultSettings(); + env.defaultUiSettings = plugins.kbnServer.uiExports.consumers + .find(consumer => consumer.getUiSettingDefaults) + .getUiSettingDefaults(); return new UiBundle({ id: 'tests', diff --git a/src/core_plugins/timelion/index.js b/src/core_plugins/timelion/index.js index c510267c823c..aee3cb9a24f9 100644 --- a/src/core_plugins/timelion/index.js +++ b/src/core_plugins/timelion/index.js @@ -36,7 +36,50 @@ export default function (kibana) { visTypes: [ 'plugins/timelion/vis' ], - mappings: require('./mappings.json') + mappings: require('./mappings.json'), + + uiSettingDefaults: { + 'timelion:showTutorial': { + value: false, + description: 'Should I show the tutorial by default when entering the timelion app?' + }, + 'timelion:es.timefield': { + value: '@timestamp', + description: 'Default field containing a timestamp when using .es()' + }, + 'timelion:es.default_index': { + value: '_all', + description: 'Default elasticsearch index to search with .es()' + }, + 'timelion:target_buckets': { + value: 200, + description: 'The number of buckets to shoot for when using auto intervals' + }, + 'timelion:max_buckets': { + value: 2000, + description: 'The maximum number of buckets a single datasource can return' + }, + 'timelion:default_columns': { + value: 2, + description: 'Number of columns on a timelion sheet by default' + }, + 'timelion:default_rows': { + value: 2, + description: 'Number of rows on a timelion sheet by default' + }, + 'timelion:min_interval': { + value: '1ms', + description: 'The smallest interval that will be calculated when using "auto"' + }, + 'timelion:graphite.url': { + value: 'https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', + description: '[experimental] The URL of your graphite host' + }, + 'timelion:quandl.key': { + value: 'someKeyHere', + description: '[experimental] Your API key from www.quandl.com' + } + } }, init: require('./init.js'), }); diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index e4d18a7b8afb..570b2fdec4a1 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -15,7 +15,7 @@ import pluginsScanMixin from './plugins/scan'; import pluginsCheckEnabledMixin from './plugins/check_enabled'; import pluginsCheckVersionMixin from './plugins/check_version'; import configCompleteMixin from './config/complete'; -import uiMixin, { uiSettingsMixin } from '../ui'; +import uiMixin from '../ui'; import optimizeMixin from '../optimize'; import pluginsInitializeMixin from './plugins/initialize'; import { indexPatternsMixin } from './index_patterns'; @@ -66,9 +66,6 @@ export default class KbnServer { // setup saved object routes savedObjectsMixin, - // setup server.uiSettings - uiSettingsMixin, - // ensure that all bundles are built, or that the // lazy bundle server is running optimizeMixin, diff --git a/src/ui/index.js b/src/ui/index.js index 8d4eb73c8511..1a978dc86f29 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -9,13 +9,15 @@ import UiBundleCollection from './ui_bundle_collection'; import UiBundlerEnv from './ui_bundler_env'; import { UiI18n } from './ui_i18n'; -export { uiSettingsMixin } from './ui_settings'; +import { uiSettingsMixin } from './ui_settings'; export default async (kbnServer, server, config) => { const uiExports = kbnServer.uiExports = new UiExports({ urlBasePath: config.get('server.basePath') }); + await kbnServer.mixin(uiSettingsMixin); + const uiI18n = kbnServer.uiI18n = new UiI18n(config.get('i18n.defaultLocale')); uiI18n.addUiExportConsumer(uiExports); diff --git a/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js index 11159061ef74..9e934afdb529 100644 --- a/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js +++ b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js @@ -51,6 +51,7 @@ describe('uiSettingsMixin()', () => { const kbnServer = { server, config, + uiExports: { addConsumer: sinon.stub() }, status: new ServerStatus(server), ready: sinon.stub().returns(readyPromise), }; @@ -133,9 +134,13 @@ describe('uiSettingsMixin()', () => { sandbox.stub(uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory'); sinon.assert.notCalled(uiSettingsServiceFactory); - const football = {}; - decorations.server.uiSettingsServiceFactory(football); - sinon.assert.calledWith(uiSettingsServiceFactory, server, football); + decorations.server.uiSettingsServiceFactory({ + foo: 'bar' + }); + sinon.assert.calledOnce(uiSettingsServiceFactory); + const { args } = uiSettingsServiceFactory.firstCall; + expect(args[0]).to.be(server); + expect(args[1]).to.have.property('foo', 'bar'); }); }); @@ -168,7 +173,10 @@ describe('uiSettingsMixin()', () => { sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest'); decorations.request.getUiSettingsService(); - const readInterceptor = getUiSettingsServiceForRequest.firstCall.args[2]; + const options = getUiSettingsServiceForRequest.firstCall.args[2]; + expect(options).to.have.property('readInterceptor'); + + const { readInterceptor } = options; expect(readInterceptor).to.be.a('function'); status.green(); diff --git a/src/ui/ui_settings/__tests__/ui_settings_service.js b/src/ui/ui_settings/__tests__/ui_settings_service.js index c7706414b0a0..ed121e004bc6 100644 --- a/src/ui/ui_settings/__tests__/ui_settings_service.js +++ b/src/ui/ui_settings/__tests__/ui_settings_service.js @@ -1,8 +1,8 @@ import { isEqual } from 'lodash'; import expect from 'expect.js'; import { errors as esErrors } from 'elasticsearch'; +import Chance from 'chance'; -import { getDefaultSettings } from '../defaults'; import { UiSettingsService } from '../ui_settings_service'; import { createCallClusterStub } from './lib'; @@ -10,10 +10,13 @@ import { createCallClusterStub } from './lib'; const INDEX = '.kibana'; const TYPE = 'config'; const ID = 'kibana-version'; +const chance = new Chance(); function setup(options = {}) { const { readInterceptor, + getDefaults, + defaults = {}, esDocSource = {}, callCluster = createCallClusterStub(INDEX, TYPE, ID, esDocSource) } = options; @@ -22,6 +25,7 @@ function setup(options = {}) { index: INDEX, type: TYPE, id: ID, + getDefaults: getDefaults || (() => defaults), readInterceptor, callCluster, }); @@ -114,34 +118,26 @@ describe('ui settings', () => { }); describe('#getDefaults()', () => { - it('is promised the default values', async () => { - const { - uiSettings - } = setup(); - const defaults = await uiSettings.getDefaults(); - expect(isEqual(defaults, getDefaultSettings())).to.equal(true); - }); - - - describe('defaults for formatters', async () => { - - const defaults = getDefaultSettings(); - const mapping = JSON.parse(defaults['format:defaultTypeMap'].value); - const expected = { - ip: { id: 'ip', params: {} }, - date: { id: 'date', params: {} }, - number: { id: 'number', params: {} }, - boolean: { id: 'boolean', params: {} }, - _source: { id: '_source', params: {} }, - _default_: { id: 'string', params: {} } - }; + it('returns a promise for the defaults', async () => { + const { uiSettings } = setup(); + const promise = uiSettings.getDefaults(); + expect(promise).to.be.a(Promise); + expect(await promise).to.eql({}); + }); + }); - Object.keys(mapping).forEach((dataType) => { - it(`should configure ${dataType}`, () => { - expect(expected.hasOwnProperty(dataType)).to.equal(true); - expect(mapping[dataType].id).to.equal(expected[dataType].id); - expect(JSON.stringify(mapping[dataType].params)).to.equal(JSON.stringify(expected[dataType].params)); - }); + describe('getDefaults() argument', () => { + it('casts sync `getDefaults()` to promise', () => { + const getDefaults = () => ({ key: { value: chance.word() } }); + const { uiSettings } = setup({ getDefaults }); + expect(uiSettings.getDefaults()).to.be.a(Promise); + }); + + it('returns the defaults returned by getDefaults() argument', async () => { + const value = chance.word(); + const { uiSettings } = setup({ defaults: { key: { value } } }); + expect(await uiSettings.getDefaults()).to.eql({ + key: { value } }); }); }); @@ -244,27 +240,26 @@ describe('ui settings', () => { it(`without user configuration it's equal to the defaults`, async () => { const esDocSource = {}; - const { uiSettings } = setup({ esDocSource }); + const defaults = { key: { value: chance.word() } }; + const { uiSettings } = setup({ esDocSource, defaults }); const result = await uiSettings.getRaw(); - expect(isEqual(result, getDefaultSettings())).to.equal(true); + expect(result).to.eql(defaults); }); it(`user configuration gets merged with defaults`, async () => { const esDocSource = { foo: 'bar' }; - const { uiSettings } = setup({ esDocSource }); + const defaults = { key: { value: chance.word() } }; + const { uiSettings } = setup({ esDocSource, defaults }); const result = await uiSettings.getRaw(); - const merged = getDefaultSettings(); - merged.foo = { userValue: 'bar' }; - expect(isEqual(result, merged)).to.equal(true); - }); - it(`user configuration gets merged into defaults`, async () => { - const esDocSource = { dateFormat: 'YYYY-MM-DD' }; - const { uiSettings } = setup({ esDocSource }); - const result = await uiSettings.getRaw(); - const merged = getDefaultSettings(); - merged.dateFormat.userValue = 'YYYY-MM-DD'; - expect(isEqual(result, merged)).to.equal(true); + expect(result).to.eql({ + foo: { + userValue: 'bar', + }, + key: { + value: defaults.key.value, + }, + }); }); }); @@ -276,42 +271,32 @@ describe('ui settings', () => { assertGetQuery(); }); - it(`returns key value pairs`, async () => { - const esDocSource = {}; - const { uiSettings } = setup({ esDocSource }); - const result = await uiSettings.getAll(); - const defaults = getDefaultSettings(); - const expectation = {}; - Object.keys(defaults).forEach((key) => { - expectation[key] = defaults[key].value; + it(`returns defaults when es doc is empty`, async () => { + const esDocSource = { }; + const defaults = { foo: { value: 'bar' } }; + const { uiSettings } = setup({ esDocSource, defaults }); + expect(await uiSettings.getAll()).to.eql({ + foo: 'bar' }); - expect(isEqual(result, expectation)).to.equal(true); }); - it(`returns key value pairs including user configuration`, async () => { - const esDocSource = { something: 'user-provided' }; - const { uiSettings } = setup({ esDocSource }); - const result = await uiSettings.getAll(); - const defaults = getDefaultSettings(); - const expectation = {}; - Object.keys(defaults).forEach((key) => { - expectation[key] = defaults[key].value; - }); - expectation.something = 'user-provided'; - expect(isEqual(result, expectation)).to.equal(true); - }); + it(`merges user values, including ones without defaults, into key value pairs`, async () => { + const esDocSource = { + foo: 'user-override', + bar: 'user-provided', + }; - it(`returns key value pairs including user configuration for existing settings`, async () => { - const esDocSource = { dateFormat: 'YYYY-MM-DD' }; - const { uiSettings } = setup({ esDocSource }); - const result = await uiSettings.getAll(); - const defaults = getDefaultSettings(); - const expectation = {}; - Object.keys(defaults).forEach((key) => { - expectation[key] = defaults[key].value; + const defaults = { + foo: { + value: 'default' + }, + }; + + const { uiSettings } = setup({ esDocSource, defaults }); + expect(await uiSettings.getAll()).to.eql({ + foo: 'user-override', + bar: 'user-provided', }); - expectation.dateFormat = 'YYYY-MM-DD'; - expect(isEqual(result, expectation)).to.equal(true); }); }); @@ -325,9 +310,9 @@ describe('ui settings', () => { it(`returns the promised value for a key`, async () => { const esDocSource = {}; - const { uiSettings } = setup({ esDocSource }); + const defaults = { dateFormat: { value: chance.word() } }; + const { uiSettings } = setup({ esDocSource, defaults }); const result = await uiSettings.get('dateFormat'); - const defaults = getDefaultSettings(); expect(result).to.equal(defaults.dateFormat.value); }); @@ -371,23 +356,21 @@ describe('ui settings', () => { describe('#getAll()', () => { it('merges intercept value with defaults', async () => { const { uiSettings } = setup({ + defaults: { + foo: { value: 'foo' }, + bar: { value: 'bar' }, + }, + readInterceptor: () => ({ foo: 'not foo' }), }); - const defaults = getDefaultSettings(); - const defaultValues = Object.keys(defaults).reduce((acc, key) => ({ - ...acc, - [key]: defaults[key].value, - }), {}); - expect(await uiSettings.getAll()).to.eql({ - ...defaultValues, foo: 'not foo', + bar: 'bar' }); }); }); }); - }); diff --git a/src/ui/ui_settings/ui_exports_consumer.js b/src/ui/ui_settings/ui_exports_consumer.js new file mode 100644 index 000000000000..433485547202 --- /dev/null +++ b/src/ui/ui_settings/ui_exports_consumer.js @@ -0,0 +1,24 @@ +export class UiExportsConsumer { + _uiSettingDefaults = { + 'buildNum': { + readonly: true + } + }; + + exportConsumer(type) { + switch (type) { + case 'uiSettingDefaults': + return (plugin, settingDefinitions) => { + this._uiSettingDefaults = { + ...this._uiSettingDefaults, + ...settingDefinitions, + }; + }; + break; + } + } + + getUiSettingDefaults() { + return this._uiSettingDefaults; + } +} diff --git a/src/ui/ui_settings/ui_settings_mixin.js b/src/ui/ui_settings/ui_settings_mixin.js index 92651ef59e7e..62b59db99154 100644 --- a/src/ui/ui_settings/ui_settings_mixin.js +++ b/src/ui/ui_settings/ui_settings_mixin.js @@ -1,10 +1,15 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request'; import { mirrorStatus } from './mirror_status'; +import { UiExportsConsumer } from './ui_exports_consumer'; export function uiSettingsMixin(kbnServer, server, config) { const status = kbnServer.status.create('ui settings'); + // reads the "uiSettingDefaults" from uiExports + const uiExportsConsumer = new UiExportsConsumer(); + kbnServer.uiExports.addConsumer(uiExportsConsumer); + if (!config.get('uiSettings.enabled')) { status.disabled('uiSettings.enabled config is set to `false`'); return; @@ -17,23 +22,33 @@ export function uiSettingsMixin(kbnServer, server, config) { // If the ui settings status isn't green we shouldn't be attempting to get // user settings, since we can't be sure that all the necessary conditions // (e.g. elasticsearch being available) are met. - const readUiSettingsInterceptor = () => { + const readInterceptor = () => { if (status.state !== 'green') { return {}; } }; + const getDefaults = () => ( + uiExportsConsumer.getUiSettingDefaults() + ); + // don't return, just let it happen when the plugins are ready kbnServer.ready().then(() => { mirrorStatus(status, kbnServer.status.getForPluginId('elasticsearch')); }); - server.decorate('server', 'uiSettingsServiceFactory', function (options) { - return uiSettingsServiceFactory(server, options); + server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { + return uiSettingsServiceFactory(server, { + getDefaults, + ...options + }); }); server.decorate('request', 'getUiSettingsService', function () { - return getUiSettingsServiceForRequest(server, this, readUiSettingsInterceptor); + return getUiSettingsServiceForRequest(server, this, { + getDefaults, + readInterceptor, + }); }); server.decorate('server', 'uiSettings', () => { diff --git a/src/ui/ui_settings/ui_settings_service.js b/src/ui/ui_settings/ui_settings_service.js index f929d79d9143..4176a67a0d8b 100644 --- a/src/ui/ui_settings/ui_settings_service.js +++ b/src/ui/ui_settings/ui_settings_service.js @@ -1,8 +1,6 @@ import { defaultsDeep, noop } from 'lodash'; import { errors as esErrors } from 'elasticsearch'; -import { getDefaultSettings } from './defaults'; - function hydrateUserSettings(userSettings) { return Object.keys(userSettings) .map(key => ({ key, userValue: userSettings[key] })) @@ -32,17 +30,19 @@ export class UiSettingsService { id, callCluster, readInterceptor = noop, + getDefaults = () => ({}), } = options; this._callCluster = callCluster; + this._getDefaults = getDefaults; this._readInterceptor = readInterceptor; this._index = index; this._type = type; this._id = id; } - getDefaults() { - return getDefaultSettings(); + async getDefaults() { + return await this._getDefaults(); } // returns a Promise for the value of the requested setting @@ -65,7 +65,7 @@ export class UiSettingsService { async getRaw() { const userProvided = await this.getUserProvided(); - return defaultsDeep(userProvided, this.getDefaults()); + return defaultsDeep(userProvided, await this.getDefaults()); } async getUserProvided(options) { diff --git a/src/ui/ui_settings/ui_settings_service_factory.js b/src/ui/ui_settings/ui_settings_service_factory.js index c300bc9bc4bf..c0b70223a3dd 100644 --- a/src/ui/ui_settings/ui_settings_service_factory.js +++ b/src/ui/ui_settings/ui_settings_service_factory.js @@ -8,6 +8,8 @@ import { UiSettingsService } from './ui_settings_service'; * @param {Object} options * @property {AsyncFunction} options.callCluster function that accepts a method name and * param object which causes a request via some elasticsearch client + * @property {AsyncFunction} [options.getDefaults] async function that returns defaults/details about + * the uiSettings. * @property {AsyncFunction} [options.readInterceptor] async function that is called when the * UiSettingsService does a read() an has an oportunity to intercept the * request and return an alternate `_source` value to use. @@ -18,7 +20,8 @@ export function uiSettingsServiceFactory(server, options) { const { callCluster, - readInterceptor + readInterceptor, + getDefaults, } = options; return new UiSettingsService({ @@ -27,5 +30,6 @@ export function uiSettingsServiceFactory(server, options) { id: config.get('pkg.version'), callCluster, readInterceptor, + getDefaults, }); } diff --git a/src/ui/ui_settings/ui_settings_service_for_request.js b/src/ui/ui_settings/ui_settings_service_for_request.js index 650bc98a5378..0a9581d7e31f 100644 --- a/src/ui/ui_settings/ui_settings_service_for_request.js +++ b/src/ui/ui_settings/ui_settings_service_for_request.js @@ -2,14 +2,36 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; const BY_REQUEST_CACHE = new WeakMap(); -export function getUiSettingsServiceForRequest(server, request, readInterceptor) { +/** + * Get/create an instance of UiSettingsService bound to a specific request. + * Each call is cached (keyed on the request object itself) and subsequent + * requests will get the first UiSettingsService instance even if the `options` + * have changed. + * + * @param {Hapi.Server} server + * @param {Hapi.Request} request + * @param {Object} [options={}] + * @property {AsyncFunction} [options.getDefaults] async function that returns defaults/details about + * the uiSettings. + * @property {AsyncFunction} [options.readInterceptor] async function that is called when the + * UiSettingsService does a read() an has an oportunity to intercept the + * request and return an alternate `_source` value to use. + * @return {UiSettingsService} + */ +export function getUiSettingsServiceForRequest(server, request, options = {}) { if (BY_REQUEST_CACHE.has(request)) { return BY_REQUEST_CACHE.get(request); } + const { + readInterceptor, + getDefaults + } = options; + const adminCluster = server.plugins.elasticsearch.getCluster('admin'); const uiSettingsService = uiSettingsServiceFactory(server, { readInterceptor, + getDefaults, callCluster(...args) { return adminCluster.callWithRequest(request, ...args); }