From fef8e646eaaddd2cf998e50f1c1aac7b1bab358c Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Mon, 20 Sep 2021 16:59:18 +0200 Subject: [PATCH 01/76] Plugins Catalog: add unit tests for data-munging (#39338) * test(Plugins/Admin): add tests for the helper functions * chore(Plugins/Admin): add `error` field to the CatalogPlugin mock * test(Plugins/Admin): add new properties `error` & `isDisabled` to test expectations * refactor(Plugins/Admin): make the `errors` param optional everywhere * fix(Plugins/Admin): fix a rebase bug --- .../admin/__mocks__/catalogPlugin.mock.ts | 1 + .../features/plugins/admin/helpers.test.ts | 667 ++++++++++++++++++ public/app/features/plugins/admin/helpers.ts | 6 +- 3 files changed, 671 insertions(+), 3 deletions(-) create mode 100644 public/app/features/plugins/admin/helpers.test.ts diff --git a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts index 5f16a962580..2d0e1d73c9f 100644 --- a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts +++ b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts @@ -27,6 +27,7 @@ export default { signatureType: 'community', updatedAt: '2021-08-25T15:03:49.000Z', version: '4.2.2', + error: undefined, details: { grafanaDependency: '>=8.0.0', pluginDependencies: [], diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts new file mode 100644 index 00000000000..15f548f2029 --- /dev/null +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -0,0 +1,667 @@ +import { RemotePlugin, LocalPlugin } from './types'; +import { getLocalPluginMock, getRemotePluginMock, getCatalogPluginMock } from './__mocks__'; +import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data'; +import { + mapToCatalogPlugin, + mapRemoteToCatalog, + mapLocalToCatalog, + mergeLocalAndRemote, + mergeLocalsAndRemotes, + sortPlugins, + Sorters, +} from './helpers'; + +describe('Plugins/Helpers', () => { + let remotePlugin: RemotePlugin; + let localPlugin: LocalPlugin; + + beforeEach(() => { + remotePlugin = getRemotePluginMock(); + localPlugin = getLocalPluginMock(); + }); + + describe('mergeLocalsAndRemotes()', () => { + const localPlugins = [ + getLocalPluginMock({ id: 'plugin-1' }), + getLocalPluginMock({ id: 'plugin-2' }), + getLocalPluginMock({ id: 'plugin-3' }), // only on local + ]; + const remotePlugins = [ + getRemotePluginMock({ slug: 'plugin-1' }), + getRemotePluginMock({ slug: 'plugin-2' }), + getRemotePluginMock({ slug: 'plugin-4' }), // only on remote + ]; + + test('adds all available plugins only once', () => { + const merged = mergeLocalsAndRemotes(localPlugins, remotePlugins); + const mergedIds = merged.map(({ id }) => id); + + expect(merged.length).toBe(4); + expect(mergedIds).toContain('plugin-1'); + expect(mergedIds).toContain('plugin-2'); + expect(mergedIds).toContain('plugin-3'); + expect(mergedIds).toContain('plugin-4'); + }); + + test('merges all plugins with their counterpart (if available)', () => { + const merged = mergeLocalsAndRemotes(localPlugins, remotePlugins); + const findMerged = (mergedId: string) => merged.find(({ id }) => id === mergedId); + + // Both local & remote counterparts + expect(findMerged('plugin-1')).toEqual( + mergeLocalAndRemote(getLocalPluginMock({ id: 'plugin-1' }), getRemotePluginMock({ slug: 'plugin-1' })) + ); + expect(findMerged('plugin-2')).toEqual( + mergeLocalAndRemote(getLocalPluginMock({ id: 'plugin-2' }), getRemotePluginMock({ slug: 'plugin-2' })) + ); + + // Only local + expect(findMerged('plugin-3')).toEqual(mergeLocalAndRemote(getLocalPluginMock({ id: 'plugin-3' }))); + + // Only remote + expect(findMerged('plugin-4')).toEqual(mergeLocalAndRemote(undefined, getRemotePluginMock({ slug: 'plugin-4' }))); + }); + }); + + describe('mergeLocalAndRemote()', () => { + test('merges using mapRemoteToCatalog() if there is only a remote version', () => { + expect(mergeLocalAndRemote(undefined, remotePlugin)).toEqual(mapRemoteToCatalog(remotePlugin)); + }); + + test('merges using mapLocalToCatalog() if there is only a local version', () => { + expect(mergeLocalAndRemote(localPlugin)).toEqual(mapLocalToCatalog(localPlugin)); + }); + + test('merges using mapToCatalogPlugin() if there is both a remote and a local version', () => { + expect(mergeLocalAndRemote(localPlugin, remotePlugin)).toEqual(mapToCatalogPlugin(localPlugin, remotePlugin)); + }); + }); + + describe('mapRemoteToCatalog()', () => { + test('maps the remote response (GCOM /api/plugins/) to PluginCatalog', () => { + expect(mapRemoteToCatalog(remotePlugin)).toEqual({ + description: 'Zabbix plugin for Grafana', + downloads: 33645089, + hasUpdate: false, + id: 'alexanderzobnin-zabbix-app', + info: { + logos: { + large: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/large', + small: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/small', + }, + }, + error: undefined, + isCore: false, + isDev: false, + isDisabled: false, + isEnterprise: false, + isInstalled: false, + name: 'Zabbix', + orgName: 'Alexander Zobnin', + popularity: 0.2111, + publishedAt: '2016-04-06T20:23:41.000Z', + signature: 'valid', + type: 'app', + updatedAt: '2021-05-18T14:53:01.000Z', + version: '4.1.5', + }); + }); + + test('adds the correct signature enum', () => { + const pluginWithoutSignature = { ...remotePlugin, signatureType: '', versionSignatureType: '' } as RemotePlugin; + // With only "signatureType" -> valid + const pluginWithSignature1 = { ...remotePlugin, signatureType: PluginSignatureType.commercial } as RemotePlugin; + // With only "versionSignatureType" -> valid + const pluginWithSignature2 = { ...remotePlugin, versionSignatureType: PluginSignatureType.core } as RemotePlugin; + + expect(mapRemoteToCatalog(pluginWithoutSignature).signature).toBe(PluginSignatureStatus.missing); + expect(mapRemoteToCatalog(pluginWithSignature1).signature).toBe(PluginSignatureStatus.valid); + expect(mapRemoteToCatalog(pluginWithSignature2).signature).toBe(PluginSignatureStatus.valid); + }); + + test('adds an "isEnterprise" field', () => { + const enterprisePlugin = { ...remotePlugin, status: 'enterprise' } as RemotePlugin; + const notEnterprisePlugin = { ...remotePlugin, status: 'unknown' } as RemotePlugin; + + expect(mapRemoteToCatalog(enterprisePlugin).isEnterprise).toBe(true); + expect(mapRemoteToCatalog(notEnterprisePlugin).isEnterprise).toBe(false); + }); + + test('adds an "isCore" field', () => { + const corePlugin = { ...remotePlugin, internal: true } as RemotePlugin; + const notCorePlugin = { ...remotePlugin, internal: false } as RemotePlugin; + + expect(mapRemoteToCatalog(corePlugin).isCore).toBe(true); + expect(mapRemoteToCatalog(notCorePlugin).isCore).toBe(false); + }); + }); + + describe('mapLocalToCatalog()', () => { + test('maps local response to PluginCatalog', () => { + expect(mapLocalToCatalog(localPlugin)).toEqual({ + description: 'Zabbix plugin for Grafana', + downloads: 0, + id: 'alexanderzobnin-zabbix-app', + info: { + logos: { + large: 'public/plugins/alexanderzobnin-zabbix-app/img/icn-zabbix-app.svg', + small: 'public/plugins/alexanderzobnin-zabbix-app/img/icn-zabbix-app.svg', + }, + }, + error: undefined, + hasUpdate: false, + isCore: false, + isDev: false, + isDisabled: false, + isEnterprise: false, + isInstalled: true, + name: 'Zabbix', + orgName: 'Alexander Zobnin', + popularity: 0, + publishedAt: '', + signature: 'valid', + signatureOrg: 'Alexander Zobnin', + signatureType: 'community', + type: 'app', + updatedAt: '2021-08-25', + version: '4.2.2', + }); + }); + + test('isCore if signature is internal', () => { + const pluginWithoutInternalSignature = { ...localPlugin }; + const pluginWithInternalSignature = { ...localPlugin, signature: 'internal' } as LocalPlugin; + expect(mapLocalToCatalog(pluginWithoutInternalSignature).isCore).toBe(false); + expect(mapLocalToCatalog(pluginWithInternalSignature).isCore).toBe(true); + }); + + test('isDev if local.dev', () => { + const pluginWithoutDev = { ...localPlugin, dev: false }; + const pluginWithDev = { ...localPlugin, dev: true }; + expect(mapLocalToCatalog(pluginWithoutDev).isDev).toBe(false); + expect(mapLocalToCatalog(pluginWithDev).isDev).toBe(true); + }); + }); + + describe('mapToCatalogPlugin()', () => { + test('merges local and remote plugin data correctly', () => { + expect(mapToCatalogPlugin(localPlugin, remotePlugin)).toEqual({ + description: 'Zabbix plugin for Grafana', + downloads: 33645089, + hasUpdate: false, + id: 'alexanderzobnin-zabbix-app', + info: { + logos: { + small: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/small', + large: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/large', + }, + }, + error: undefined, + isCore: false, + isDev: false, + isDisabled: false, + isEnterprise: false, + isInstalled: true, + name: 'Zabbix', + orgName: 'Alexander Zobnin', + popularity: 0.2111, + publishedAt: '2016-04-06T20:23:41.000Z', + signature: 'valid', + signatureOrg: 'Alexander Zobnin', + signatureType: 'community', + type: 'app', + updatedAt: '2021-05-18T14:53:01.000Z', + version: '4.1.5', + }); + }); + + test('`.description` - prefers the remote', () => { + // Local & Remote + expect( + mapToCatalogPlugin( + { ...localPlugin, info: { ...localPlugin.info, description: 'Local description' } }, + { ...remotePlugin, description: 'Remote description' } + ) + ).toMatchObject({ description: 'Remote description' }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, description: 'Remote description' })).toMatchObject({ + description: 'Remote description', + }); + + // Local only + expect( + mapToCatalogPlugin({ ...localPlugin, info: { ...localPlugin.info, description: 'Local description' } }) + ).toMatchObject({ description: 'Local description' }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ description: '' }); + }); + + test('`.hasUpdate` - prefers the local', () => { + // Local & Remote (only if the remote version is greater than the local one) + expect( + mapToCatalogPlugin( + { ...localPlugin, info: { ...localPlugin.info, version: '2.0.0' } }, + { ...remotePlugin, version: '2.1.0' } + ) + ).toMatchObject({ hasUpdate: true }); + expect( + mapToCatalogPlugin( + { ...localPlugin, info: { ...localPlugin.info, version: '2.1.0' } }, + { ...remotePlugin, version: '2.1.0' } + ) + ).toMatchObject({ hasUpdate: false }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, version: '2.1.0' })).toMatchObject({ + hasUpdate: false, + }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin })).toMatchObject({ hasUpdate: false }); + expect(mapToCatalogPlugin({ ...localPlugin, hasUpdate: true })).toMatchObject({ hasUpdate: true }); + expect(mapToCatalogPlugin({ ...localPlugin, info: { ...localPlugin.info, version: '2.1.0' } })).toMatchObject({ + hasUpdate: false, + }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ hasUpdate: false }); + }); + + test('`.downloads` - relies on the remote', () => { + // Local & Remote + expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, downloads: 99 })).toMatchObject({ downloads: 99 }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, downloads: 99 })).toMatchObject({ downloads: 99 }); + + // Local only + expect(mapToCatalogPlugin(localPlugin)).toMatchObject({ downloads: 0 }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ downloads: 0 }); + }); + + test('`.isCore` - prefers the remote', () => { + // Local & Remote + expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, internal: true })).toMatchObject({ isCore: true }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, internal: true })).toMatchObject({ isCore: true }); + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, internal: false })).toMatchObject({ isCore: false }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin, signature: PluginSignatureStatus.internal })).toMatchObject({ + isCore: true, + }); + expect(mapToCatalogPlugin({ ...localPlugin, signature: PluginSignatureStatus.valid })).toMatchObject({ + isCore: false, + }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ isCore: false }); + }); + + test('`.isDev` - prefers the local', () => { + // Local & Remote + expect(mapToCatalogPlugin({ ...localPlugin, dev: true }, remotePlugin)).toMatchObject({ isDev: true }); + + // Remote only + expect(mapToCatalogPlugin(undefined, remotePlugin)).toMatchObject({ isDev: false }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin, dev: true }, undefined)).toMatchObject({ isDev: true }); + expect(mapToCatalogPlugin({ ...localPlugin, dev: undefined }, undefined)).toMatchObject({ isDev: false }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ isDev: false }); + }); + + test('`.isEnterprise` - prefers the remote', () => { + // Local & Remote + expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: 'enterprise' })).toMatchObject({ + isEnterprise: true, + }); + expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, status: 'unknown' })).toMatchObject({ + isEnterprise: false, + }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, status: 'enterprise' })).toMatchObject({ + isEnterprise: true, + }); + + // Local only + expect(mapToCatalogPlugin(localPlugin)).toMatchObject({ isEnterprise: false }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ isEnterprise: false }); + }); + + test('`.isInstalled` - prefers the local', () => { + // Local & Remote + expect(mapToCatalogPlugin(localPlugin, remotePlugin)).toMatchObject({ isInstalled: true }); + + // Remote only + expect(mapToCatalogPlugin(undefined, remotePlugin)).toMatchObject({ isInstalled: false }); + + // Local only + expect(mapToCatalogPlugin(localPlugin, undefined)).toMatchObject({ isInstalled: true }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ isInstalled: false }); + }); + + test('`.name` - prefers the remote', () => { + // Local & Remote + expect( + mapToCatalogPlugin({ ...localPlugin, name: 'Local name' }, { ...remotePlugin, name: 'Remote name' }) + ).toMatchObject({ name: 'Remote name' }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, name: 'Remote name' })).toMatchObject({ + name: 'Remote name', + }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin, name: 'Local name' })).toMatchObject({ name: 'Local name' }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ name: '' }); + }); + + test('`.orgName` - prefers the remote', () => { + // Local & Remote + expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, orgName: 'Remote org' })).toMatchObject({ + orgName: 'Remote org', + }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, orgName: 'Remote org' })).toMatchObject({ + orgName: 'Remote org', + }); + + // Local only + expect(mapToCatalogPlugin(localPlugin)).toMatchObject({ orgName: 'Alexander Zobnin' }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ orgName: '' }); + }); + + test('`.popularity` - prefers the remote', () => { + // Local & Remote + expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, popularity: 10 })).toMatchObject({ popularity: 10 }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, popularity: 10 })).toMatchObject({ popularity: 10 }); + + // Local only + expect(mapToCatalogPlugin(localPlugin)).toMatchObject({ popularity: 0 }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ popularity: 0 }); + }); + + test('`.publishedAt` - prefers the remote', () => { + // Local & Remote + expect(mapToCatalogPlugin(localPlugin, { ...remotePlugin, createdAt: '2020-01-01' })).toMatchObject({ + publishedAt: '2020-01-01', + }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, createdAt: '2020-01-01' })).toMatchObject({ + publishedAt: '2020-01-01', + }); + + // Local only + expect(mapToCatalogPlugin(localPlugin)).toMatchObject({ publishedAt: '' }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ publishedAt: '' }); + }); + + test('`.type` - prefers the remote', () => { + // Local & Remote + expect( + mapToCatalogPlugin( + { ...localPlugin, type: PluginType.app }, + { ...remotePlugin, typeCode: PluginType.datasource } + ) + ).toMatchObject({ + type: PluginType.datasource, + }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, typeCode: PluginType.datasource })).toMatchObject({ + type: PluginType.datasource, + }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin, type: PluginType.app })).toMatchObject({ + type: PluginType.app, + }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ type: undefined }); + }); + + test('`.signature` - prefers the local', () => { + // Local & Remote + expect( + mapToCatalogPlugin( + { ...localPlugin, signature: PluginSignatureStatus.valid }, + { ...remotePlugin, signatureType: '', versionSignatureType: '' } + ) + ).toMatchObject({ + signature: PluginSignatureStatus.valid, + }); + expect( + mapToCatalogPlugin( + { ...localPlugin, signature: PluginSignatureStatus.missing }, + { + ...remotePlugin, + signatureType: PluginSignatureType.grafana, + versionSignatureType: PluginSignatureType.grafana, + } + ) + ).toMatchObject({ + signature: PluginSignatureStatus.missing, + }); + + // Remote only + expect( + mapToCatalogPlugin(undefined, { ...remotePlugin, signatureType: PluginSignatureType.grafana }) + ).toMatchObject({ + signature: PluginSignatureStatus.valid, + }); + expect( + mapToCatalogPlugin(undefined, { ...remotePlugin, versionSignatureType: PluginSignatureType.grafana }) + ).toMatchObject({ + signature: PluginSignatureStatus.valid, + }); + expect( + mapToCatalogPlugin(undefined, { ...remotePlugin, signatureType: '', versionSignatureType: '' }) + ).toMatchObject({ + signature: PluginSignatureStatus.missing, + }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin, signature: PluginSignatureStatus.valid })).toMatchObject({ + signature: PluginSignatureStatus.valid, + }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ signature: PluginSignatureStatus.missing }); + }); + + test('`.signatureOrg` - prefers the local', () => { + // Local & Remote + expect( + mapToCatalogPlugin( + { ...localPlugin, signatureOrg: 'Local Org' }, + { ...remotePlugin, versionSignedByOrgName: 'Remote Org' } + ) + ).toMatchObject({ + signatureOrg: 'Local Org', + }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, versionSignedByOrgName: 'Remote Org' })).toMatchObject({ + signatureOrg: 'Remote Org', + }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin, signatureOrg: 'Local Org' })).toMatchObject({ + signatureOrg: 'Local Org', + }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ signatureOrg: undefined }); + }); + + test('`.signatureType` - prefers the local', () => { + // Local & Remote + expect( + mapToCatalogPlugin( + { ...localPlugin, signatureType: PluginSignatureType.core }, + { ...remotePlugin, signatureType: PluginSignatureType.grafana } + ) + ).toMatchObject({ + signatureType: PluginSignatureType.core, + }); + + // Remote only + expect( + mapToCatalogPlugin(undefined, { + ...remotePlugin, + versionSignatureType: PluginSignatureType.core, + signatureType: PluginSignatureType.grafana, + }) + ).toMatchObject({ + signatureType: PluginSignatureType.core, + }); + expect( + mapToCatalogPlugin(undefined, { + ...remotePlugin, + versionSignatureType: '', + signatureType: PluginSignatureType.grafana, + }) + ).toMatchObject({ + signatureType: PluginSignatureType.grafana, + }); + + // Local only + expect(mapToCatalogPlugin({ ...localPlugin, signatureType: PluginSignatureType.core })).toMatchObject({ + signatureType: PluginSignatureType.core, + }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ signatureType: undefined }); + }); + + test('`.updatedAt` - prefers the remote', () => { + // Local & Remote + expect( + mapToCatalogPlugin( + { ...localPlugin, info: { ...localPlugin.info, updated: '2019-01-01' } }, + { ...remotePlugin, updatedAt: '2020-01-01' } + ) + ).toMatchObject({ + updatedAt: '2020-01-01', + }); + + // Remote only + expect(mapToCatalogPlugin(undefined, { ...remotePlugin, updatedAt: '2020-01-01' })).toMatchObject({ + updatedAt: '2020-01-01', + }); + + // Local only + expect( + mapToCatalogPlugin({ ...localPlugin, info: { ...localPlugin.info, updated: '2019-01-01' } }) + ).toMatchObject({ + updatedAt: '2019-01-01', + }); + + // No local or remote + expect(mapToCatalogPlugin()).toMatchObject({ updatedAt: '' }); + }); + }); + + describe('sortPlugins()', () => { + test('should be possible to sort by `name` ASC', () => { + const sorted = sortPlugins( + [ + getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }), + getCatalogPluginMock({ id: 'snowflake', name: 'Snowflake' }), + getCatalogPluginMock({ id: 'jira', name: 'Jira' }), + getCatalogPluginMock({ id: 'pie-chart', name: 'Pie Chart' }), + getCatalogPluginMock({ id: 'cloud-watch', name: 'CloudWatch' }), + ], + Sorters.nameAsc + ); + + expect(sorted.map(({ name }) => name)).toEqual(['CloudWatch', 'Jira', 'Pie Chart', 'Snowflake', 'Zabbix']); + }); + + test('should be possible to sort by `name` DESC', () => { + const sorted = sortPlugins( + [ + getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }), + getCatalogPluginMock({ id: 'snowflake', name: 'Snowflake' }), + getCatalogPluginMock({ id: 'jira', name: 'Jira' }), + getCatalogPluginMock({ id: 'pie-chart', name: 'Pie Chart' }), + getCatalogPluginMock({ id: 'cloud-watch', name: 'CloudWatch' }), + ], + Sorters.nameDesc + ); + + expect(sorted.map(({ name }) => name)).toEqual(['Zabbix', 'Snowflake', 'Pie Chart', 'Jira', 'CloudWatch']); + }); + + test('should be possible to sort by `updated` (latest first)', () => { + const sorted = sortPlugins( + [ + getCatalogPluginMock({ id: 'zabbix', updatedAt: '2010-01-01' }), + getCatalogPluginMock({ id: 'snowflake', updatedAt: '2012-01-01' }), + getCatalogPluginMock({ id: 'jira', updatedAt: '2005-01-01' }), + getCatalogPluginMock({ id: 'pie-chart', updatedAt: '2021-01-01' }), + getCatalogPluginMock({ id: 'cloud-watch', updatedAt: '2009-01-01' }), + ], + Sorters.updated + ); + + expect(sorted.map(({ id }) => id)).toEqual(['pie-chart', 'snowflake', 'zabbix', 'cloud-watch', 'jira']); + }); + + test('should be possible to sort by `published` (latest first)', () => { + const sorted = sortPlugins( + [ + getCatalogPluginMock({ id: 'zabbix', publishedAt: '2010-01-01' }), + getCatalogPluginMock({ id: 'snowflake', publishedAt: '2012-01-01' }), + getCatalogPluginMock({ id: 'jira', publishedAt: '2005-01-01' }), + getCatalogPluginMock({ id: 'pie-chart', publishedAt: '2021-01-01' }), + getCatalogPluginMock({ id: 'cloud-watch', publishedAt: '2009-01-01' }), + ], + Sorters.published + ); + + expect(sorted.map(({ id }) => id)).toEqual(['pie-chart', 'snowflake', 'zabbix', 'cloud-watch', 'jira']); + }); + + test('should be possible to sort by `downloads` (greatest first)', () => { + const sorted = sortPlugins( + [ + getCatalogPluginMock({ id: 'zabbix', downloads: 30 }), + getCatalogPluginMock({ id: 'snowflake', downloads: 10 }), + getCatalogPluginMock({ id: 'jira', downloads: 100 }), + getCatalogPluginMock({ id: 'pie-chart', downloads: 350 }), + getCatalogPluginMock({ id: 'cloud-watch', downloads: 200 }), + ], + Sorters.downloads + ); + + expect(sorted.map(({ id }) => id)).toEqual(['pie-chart', 'cloud-watch', 'jira', 'zabbix', 'snowflake']); + }); + }); +}); diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index c0c591efe01..e6c9b460916 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -17,7 +17,7 @@ export function isOrgAdmin() { export function mergeLocalsAndRemotes( local: LocalPlugin[] = [], remote: RemotePlugin[] = [], - errors: PluginError[] + errors?: PluginError[] ): CatalogPlugin[] { const catalogPlugins: CatalogPlugin[] = []; const errorByPluginId = groupErrorsByPluginId(errors); @@ -145,7 +145,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e const hasUpdate = local?.hasUpdate || Boolean(remote?.version && local?.info.version && gt(remote?.version, local?.info.version)); const id = remote?.slug || local?.id || ''; - const hasRemoteSignature = remote?.signatureType !== '' || remote?.versionSignatureType !== ''; + const hasRemoteSignature = remote?.signatureType || remote?.versionSignatureType; const isDisabled = !!error; let logos = { @@ -217,7 +217,7 @@ export const sortPlugins = (plugins: CatalogPlugin[], sortBy: Sorters) => { return plugins; }; -function groupErrorsByPluginId(errors: PluginError[]): Record { +function groupErrorsByPluginId(errors: PluginError[] = []): Record { return errors.reduce((byId, error) => { byId[error.pluginId] = error; return byId; From 1e4a660ea71fd99c987db6b3a30315a0c90171dd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 20 Sep 2021 17:05:30 +0200 Subject: [PATCH 02/76] Chore: Propagate context for quotas (#39205) Propagate context for quotas and queries/commands. Ref #36734 --- pkg/api/quota.go | 8 +- pkg/middleware/quota_test.go | 8 +- pkg/services/quota/quota.go | 6 +- pkg/services/sqlstore/quota.go | 347 +++++++++--------- pkg/services/sqlstore/quota_test.go | 57 +-- pkg/services/sqlstore/sqlstore.go | 1 + .../api/alerting/api_alertmanager_test.go | 6 +- 7 files changed, 222 insertions(+), 211 deletions(-) diff --git a/pkg/api/quota.go b/pkg/api/quota.go index c78626c4ea1..9f0ff492fd1 100644 --- a/pkg/api/quota.go +++ b/pkg/api/quota.go @@ -14,7 +14,7 @@ func GetOrgQuotas(c *models.ReqContext) response.Response { } query := models.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")} - if err := bus.Dispatch(&query); err != nil { + if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil { return response.Error(500, "Failed to get org quotas", err) } @@ -32,7 +32,7 @@ func UpdateOrgQuota(c *models.ReqContext, cmd models.UpdateOrgQuotaCmd) response return response.Error(404, "Invalid quota target", nil) } - if err := bus.Dispatch(&cmd); err != nil { + if err := bus.DispatchCtx(c.Req.Context(), &cmd); err != nil { return response.Error(500, "Failed to update org quotas", err) } return response.Success("Organization quota updated") @@ -44,7 +44,7 @@ func GetUserQuotas(c *models.ReqContext) response.Response { } query := models.GetUserQuotasQuery{UserId: c.ParamsInt64(":id")} - if err := bus.Dispatch(&query); err != nil { + if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil { return response.Error(500, "Failed to get org quotas", err) } @@ -62,7 +62,7 @@ func UpdateUserQuota(c *models.ReqContext, cmd models.UpdateUserQuotaCmd) respon return response.Error(404, "Invalid quota target", nil) } - if err := bus.Dispatch(&cmd); err != nil { + if err := bus.DispatchCtx(c.Req.Context(), &cmd); err != nil { return response.Error(500, "Failed to update org quotas", err) } return response.Success("Organization quota updated") diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 2319c470191..c719e99b5da 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -16,7 +16,7 @@ import ( func TestMiddlewareQuota(t *testing.T) { t.Run("With user not logged in", func(t *testing.T) { middlewareScenario(t, "and global quota not reached", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error { query.Result = &models.GlobalQuotaDTO{ Target: query.Target, Limit: query.Default, @@ -33,7 +33,7 @@ func TestMiddlewareQuota(t *testing.T) { }, configure) middlewareScenario(t, "and global quota reached", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error { query.Result = &models.GlobalQuotaDTO{ Target: query.Target, Limit: query.Default, @@ -53,7 +53,7 @@ func TestMiddlewareQuota(t *testing.T) { }) middlewareScenario(t, "and global session quota not reached", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error { query.Result = &models.GlobalQuotaDTO{ Target: query.Target, Limit: query.Default, @@ -101,7 +101,7 @@ func TestMiddlewareQuota(t *testing.T) { }, nil } - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error { query.Result = &models.GlobalQuotaDTO{ Target: query.Target, Limit: query.Default, diff --git a/pkg/services/quota/quota.go b/pkg/services/quota/quota.go index 7cb1cb115bd..7e2a923ceeb 100644 --- a/pkg/services/quota/quota.go +++ b/pkg/services/quota/quota.go @@ -63,7 +63,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool, continue } query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()} - if err := bus.Dispatch(&query); err != nil { + if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil { return true, err } if query.Result.Used >= scope.DefaultLimit { @@ -79,7 +79,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool, Default: scope.DefaultLimit, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled(), } - if err := bus.Dispatch(&query); err != nil { + if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil { return true, err } if query.Result.Limit < 0 { @@ -97,7 +97,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool, continue } query := models.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()} - if err := bus.Dispatch(&query); err != nil { + if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil { return true, err } if query.Result.Limit < 0 { diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 24d3bb9a802..08c85f076d4 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -1,6 +1,7 @@ package sqlstore import ( + "context" "fmt" "time" @@ -14,108 +15,111 @@ const ( dashboardTarget = "dashboard" ) -func init() { - bus.AddHandler("sql", GetOrgQuotaByTarget) - bus.AddHandler("sql", GetOrgQuotas) - bus.AddHandler("sql", UpdateOrgQuota) - bus.AddHandler("sql", GetUserQuotaByTarget) - bus.AddHandler("sql", GetUserQuotas) - bus.AddHandler("sql", UpdateUserQuota) - bus.AddHandler("sql", GetGlobalQuotaByTarget) +func (ss *SQLStore) addQuotaQueryAndCommandHandlers() { + bus.AddHandlerCtx("sql", ss.GetOrgQuotaByTarget) + bus.AddHandlerCtx("sql", ss.GetOrgQuotas) + bus.AddHandlerCtx("sql", ss.UpdateOrgQuota) + bus.AddHandlerCtx("sql", ss.GetUserQuotaByTarget) + bus.AddHandlerCtx("sql", ss.GetUserQuotas) + bus.AddHandlerCtx("sql", ss.UpdateUserQuota) + bus.AddHandlerCtx("sql", ss.GetGlobalQuotaByTarget) } type targetCount struct { Count int64 } -func GetOrgQuotaByTarget(query *models.GetOrgQuotaByTargetQuery) error { - quota := models.Quota{ - Target: query.Target, - OrgId: query.OrgId, - } - has, err := x.Get("a) - if err != nil { - return err - } else if !has { - quota.Limit = query.Default - } - - var used int64 - if query.Target != alertRuleTarget || query.IsNgAlertEnabled { - // get quota used. - rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM %s WHERE org_id=?", - dialect.Quote(query.Target)) - - if query.Target == dashboardTarget { - rawSQL += fmt.Sprintf(" AND is_folder=%s", dialect.BooleanStr(false)) +func (ss *SQLStore) GetOrgQuotaByTarget(ctx context.Context, query *models.GetOrgQuotaByTargetQuery) error { + return ss.WithDbSession(ctx, func(sess *DBSession) error { + quota := models.Quota{ + Target: query.Target, + OrgId: query.OrgId, } - - resp := make([]*targetCount, 0) - if err := x.SQL(rawSQL, query.OrgId).Find(&resp); err != nil { + has, err := sess.Get("a) + if err != nil { return err + } else if !has { + quota.Limit = query.Default } - used = resp[0].Count - } - query.Result = &models.OrgQuotaDTO{ - Target: query.Target, - Limit: quota.Limit, - OrgId: query.OrgId, - Used: used, - } - - return nil -} - -func GetOrgQuotas(query *models.GetOrgQuotasQuery) error { - quotas := make([]*models.Quota, 0) - sess := x.Table("quota") - if err := sess.Where("org_id=? AND user_id=0", query.OrgId).Find("as); err != nil { - return err - } - - defaultQuotas := setting.Quota.Org.ToMap() - - seenTargets := make(map[string]bool) - for _, q := range quotas { - seenTargets[q.Target] = true - } - - for t, v := range defaultQuotas { - if _, ok := seenTargets[t]; !ok { - quotas = append(quotas, &models.Quota{ - OrgId: query.OrgId, - Target: t, - Limit: v, - }) - } - } - - result := make([]*models.OrgQuotaDTO, len(quotas)) - for i, q := range quotas { var used int64 - if q.Target != alertRuleTarget || query.IsNgAlertEnabled { + if query.Target != alertRuleTarget || query.IsNgAlertEnabled { // get quota used. - rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target)) + rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM %s WHERE org_id=?", + dialect.Quote(query.Target)) + + if query.Target == dashboardTarget { + rawSQL += fmt.Sprintf(" AND is_folder=%s", dialect.BooleanStr(false)) + } + resp := make([]*targetCount, 0) - if err := x.SQL(rawSQL, q.OrgId).Find(&resp); err != nil { + if err := sess.SQL(rawSQL, query.OrgId).Find(&resp); err != nil { return err } used = resp[0].Count } - result[i] = &models.OrgQuotaDTO{ - Target: q.Target, - Limit: q.Limit, - OrgId: q.OrgId, + + query.Result = &models.OrgQuotaDTO{ + Target: query.Target, + Limit: quota.Limit, + OrgId: query.OrgId, Used: used, } - } - query.Result = result - return nil + + return nil + }) } -func UpdateOrgQuota(cmd *models.UpdateOrgQuotaCmd) error { - return inTransaction(func(sess *DBSession) error { +func (ss *SQLStore) GetOrgQuotas(ctx context.Context, query *models.GetOrgQuotasQuery) error { + return ss.WithDbSession(ctx, func(sess *DBSession) error { + quotas := make([]*models.Quota, 0) + if err := sess.Table("quota").Where("org_id=? AND user_id=0", query.OrgId).Find("as); err != nil { + return err + } + + defaultQuotas := setting.Quota.Org.ToMap() + + seenTargets := make(map[string]bool) + for _, q := range quotas { + seenTargets[q.Target] = true + } + + for t, v := range defaultQuotas { + if _, ok := seenTargets[t]; !ok { + quotas = append(quotas, &models.Quota{ + OrgId: query.OrgId, + Target: t, + Limit: v, + }) + } + } + + result := make([]*models.OrgQuotaDTO, len(quotas)) + for i, q := range quotas { + var used int64 + if q.Target != alertRuleTarget || query.IsNgAlertEnabled { + // get quota used. + rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target)) + resp := make([]*targetCount, 0) + if err := sess.SQL(rawSQL, q.OrgId).Find(&resp); err != nil { + return err + } + used = resp[0].Count + } + result[i] = &models.OrgQuotaDTO{ + Target: q.Target, + Limit: q.Limit, + OrgId: q.OrgId, + Used: used, + } + } + query.Result = result + return nil + }) +} + +func (ss *SQLStore) UpdateOrgQuota(ctx context.Context, cmd *models.UpdateOrgQuotaCmd) error { + return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { // Check if quota is already defined in the DB quota := models.Quota{ Target: cmd.Target, @@ -145,88 +149,91 @@ func UpdateOrgQuota(cmd *models.UpdateOrgQuotaCmd) error { }) } -func GetUserQuotaByTarget(query *models.GetUserQuotaByTargetQuery) error { - quota := models.Quota{ - Target: query.Target, - UserId: query.UserId, - } - has, err := x.Get("a) - if err != nil { - return err - } else if !has { - quota.Limit = query.Default - } - - var used int64 - if query.Target != alertRuleTarget || query.IsNgAlertEnabled { - // get quota used. - rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target)) - resp := make([]*targetCount, 0) - if err := x.SQL(rawSQL, query.UserId).Find(&resp); err != nil { +func (ss *SQLStore) GetUserQuotaByTarget(ctx context.Context, query *models.GetUserQuotaByTargetQuery) error { + return ss.WithDbSession(ctx, func(sess *DBSession) error { + quota := models.Quota{ + Target: query.Target, + UserId: query.UserId, + } + has, err := sess.Get("a) + if err != nil { return err + } else if !has { + quota.Limit = query.Default } - used = resp[0].Count - } - query.Result = &models.UserQuotaDTO{ - Target: query.Target, - Limit: quota.Limit, - UserId: query.UserId, - Used: used, - } - - return nil -} - -func GetUserQuotas(query *models.GetUserQuotasQuery) error { - quotas := make([]*models.Quota, 0) - sess := x.Table("quota") - if err := sess.Where("user_id=? AND org_id=0", query.UserId).Find("as); err != nil { - return err - } - - defaultQuotas := setting.Quota.User.ToMap() - - seenTargets := make(map[string]bool) - for _, q := range quotas { - seenTargets[q.Target] = true - } - - for t, v := range defaultQuotas { - if _, ok := seenTargets[t]; !ok { - quotas = append(quotas, &models.Quota{ - UserId: query.UserId, - Target: t, - Limit: v, - }) - } - } - - result := make([]*models.UserQuotaDTO, len(quotas)) - for i, q := range quotas { var used int64 - if q.Target != alertRuleTarget || query.IsNgAlertEnabled { + if query.Target != alertRuleTarget || query.IsNgAlertEnabled { // get quota used. - rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target)) + rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target)) resp := make([]*targetCount, 0) - if err := x.SQL(rawSQL, q.UserId).Find(&resp); err != nil { + if err := sess.SQL(rawSQL, query.UserId).Find(&resp); err != nil { return err } used = resp[0].Count } - result[i] = &models.UserQuotaDTO{ - Target: q.Target, - Limit: q.Limit, - UserId: q.UserId, + + query.Result = &models.UserQuotaDTO{ + Target: query.Target, + Limit: quota.Limit, + UserId: query.UserId, Used: used, } - } - query.Result = result - return nil + + return nil + }) } -func UpdateUserQuota(cmd *models.UpdateUserQuotaCmd) error { - return inTransaction(func(sess *DBSession) error { +func (ss *SQLStore) GetUserQuotas(ctx context.Context, query *models.GetUserQuotasQuery) error { + return ss.WithDbSession(ctx, func(sess *DBSession) error { + quotas := make([]*models.Quota, 0) + if err := sess.Table("quota").Where("user_id=? AND org_id=0", query.UserId).Find("as); err != nil { + return err + } + + defaultQuotas := setting.Quota.User.ToMap() + + seenTargets := make(map[string]bool) + for _, q := range quotas { + seenTargets[q.Target] = true + } + + for t, v := range defaultQuotas { + if _, ok := seenTargets[t]; !ok { + quotas = append(quotas, &models.Quota{ + UserId: query.UserId, + Target: t, + Limit: v, + }) + } + } + + result := make([]*models.UserQuotaDTO, len(quotas)) + for i, q := range quotas { + var used int64 + if q.Target != alertRuleTarget || query.IsNgAlertEnabled { + // get quota used. + rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target)) + resp := make([]*targetCount, 0) + if err := sess.SQL(rawSQL, q.UserId).Find(&resp); err != nil { + return err + } + used = resp[0].Count + } + result[i] = &models.UserQuotaDTO{ + Target: q.Target, + Limit: q.Limit, + UserId: q.UserId, + Used: used, + } + } + query.Result = result + return nil + }) +} + +func (ss *SQLStore) UpdateUserQuota(ctx context.Context, cmd *models.UpdateUserQuotaCmd) error { + return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { // Check if quota is already defined in the DB quota := models.Quota{ Target: cmd.Target, @@ -256,29 +263,31 @@ func UpdateUserQuota(cmd *models.UpdateUserQuotaCmd) error { }) } -func GetGlobalQuotaByTarget(query *models.GetGlobalQuotaByTargetQuery) error { - var used int64 - if query.Target != alertRuleTarget || query.IsNgAlertEnabled { - // get quota used. - rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM %s", - dialect.Quote(query.Target)) +func (ss *SQLStore) GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error { + return ss.WithDbSession(ctx, func(sess *DBSession) error { + var used int64 + if query.Target != alertRuleTarget || query.IsNgAlertEnabled { + // get quota used. + rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM %s", + dialect.Quote(query.Target)) - if query.Target == dashboardTarget { - rawSQL += fmt.Sprintf(" WHERE is_folder=%s", dialect.BooleanStr(false)) + if query.Target == dashboardTarget { + rawSQL += fmt.Sprintf(" WHERE is_folder=%s", dialect.BooleanStr(false)) + } + + resp := make([]*targetCount, 0) + if err := sess.SQL(rawSQL).Find(&resp); err != nil { + return err + } + used = resp[0].Count } - resp := make([]*targetCount, 0) - if err := x.SQL(rawSQL).Find(&resp); err != nil { - return err + query.Result = &models.GlobalQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: used, } - used = resp[0].Count - } - query.Result = &models.GlobalQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: used, - } - - return nil + return nil + }) } diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index e2751f81c98..81a24316b94 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -4,6 +4,7 @@ package sqlstore import ( + "context" "testing" "time" @@ -13,7 +14,7 @@ import ( ) func TestQuotaCommandsAndQueries(t *testing.T) { - InitTestDB(t) + sqlStore := InitTestDB(t) userId := int64(1) orgId := int64(0) @@ -58,12 +59,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) { Target: "org_user", Limit: 10, } - err := UpdateOrgQuota(&orgCmd) + err := sqlStore.UpdateOrgQuota(context.Background(), &orgCmd) require.NoError(t, err) t.Run("Should be able to get saved quota by org id and target", func(t *testing.T) { query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(10), query.Result.Limit) @@ -71,7 +72,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get default quota by org id and target", func(t *testing.T) { query := models.GetOrgQuotaByTargetQuery{OrgId: 123, Target: "org_user", Default: 11} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(11), query.Result.Limit) @@ -79,7 +80,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get used org quota when rows exist", func(t *testing.T) { query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 11} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(1), query.Result.Used) @@ -87,7 +88,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get used org quota when no rows exist", func(t *testing.T) { query := models.GetOrgQuotaByTargetQuery{OrgId: 2, Target: "org_user", Default: 11} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(0), query.Result.Used) @@ -95,7 +96,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get zero used org alert quota when table does not exist (ngalert is not enabled - default case)", func(t *testing.T) { query := models.GetOrgQuotaByTargetQuery{OrgId: 2, Target: "alert", Default: 11} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(0), query.Result.Used) @@ -103,7 +104,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to quota list for org", func(t *testing.T) { query := models.GetOrgQuotasQuery{OrgId: orgId} - err = GetOrgQuotas(&query) + err = sqlStore.GetOrgQuotas(context.Background(), &query) require.NoError(t, err) require.Len(t, query.Result, 5) @@ -126,12 +127,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) { Target: dashboardTarget, Limit: 10, } - err := UpdateOrgQuota(&orgCmd) + err := sqlStore.UpdateOrgQuota(context.Background(), &orgCmd) require.NoError(t, err) t.Run("Should be able to get saved quota by org id and target", func(t *testing.T) { query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: dashboardTarget, Default: 1} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(10), query.Result.Limit) @@ -145,12 +146,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) { Target: "org_user", Limit: 10, } - err := UpdateUserQuota(&userQuotaCmd) + err := sqlStore.UpdateUserQuota(context.Background(), &userQuotaCmd) require.NoError(t, err) t.Run("Should be able to get saved quota by user id and target", func(t *testing.T) { query := models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1} - err = GetUserQuotaByTarget(&query) + err = sqlStore.GetUserQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(10), query.Result.Limit) @@ -158,7 +159,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get default quota by user id and target", func(t *testing.T) { query := models.GetUserQuotaByTargetQuery{UserId: 9, Target: "org_user", Default: 11} - err = GetUserQuotaByTarget(&query) + err = sqlStore.GetUserQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(11), query.Result.Limit) @@ -166,7 +167,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get used user quota when rows exist", func(t *testing.T) { query := models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 11} - err = GetUserQuotaByTarget(&query) + err = sqlStore.GetUserQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(1), query.Result.Used) @@ -174,7 +175,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get used user quota when no rows exist", func(t *testing.T) { query := models.GetUserQuotaByTargetQuery{UserId: 2, Target: "org_user", Default: 11} - err = GetUserQuotaByTarget(&query) + err = sqlStore.GetUserQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(0), query.Result.Used) @@ -182,7 +183,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to quota list for user", func(t *testing.T) { query := models.GetUserQuotasQuery{UserId: userId} - err = GetUserQuotas(&query) + err = sqlStore.GetUserQuotas(context.Background(), &query) require.NoError(t, err) require.Len(t, query.Result, 1) @@ -193,7 +194,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to global user quota", func(t *testing.T) { query := models.GetGlobalQuotaByTargetQuery{Target: "user", Default: 5} - err = GetGlobalQuotaByTarget(&query) + err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(5), query.Result.Limit) @@ -202,7 +203,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to global org quota", func(t *testing.T) { query := models.GetGlobalQuotaByTargetQuery{Target: "org", Default: 5} - err = GetGlobalQuotaByTarget(&query) + err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(5), query.Result.Limit) @@ -211,7 +212,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to get zero used global alert quota when table does not exist (ngalert is not enabled - default case)", func(t *testing.T) { query := models.GetGlobalQuotaByTargetQuery{Target: "alert_rule", Default: 5} - err = GetGlobalQuotaByTarget(&query) + err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(5), query.Result.Limit) @@ -220,7 +221,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { t.Run("Should be able to global dashboard quota", func(t *testing.T) { query := models.GetGlobalQuotaByTargetQuery{Target: dashboardTarget, Default: 5} - err = GetGlobalQuotaByTarget(&query) + err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(5), query.Result.Limit) @@ -234,11 +235,11 @@ func TestQuotaCommandsAndQueries(t *testing.T) { Target: "org_user", Limit: 5, } - err := UpdateOrgQuota(&orgCmd) + err := sqlStore.UpdateOrgQuota(context.Background(), &orgCmd) require.NoError(t, err) query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(5), query.Result.Limit) @@ -250,11 +251,11 @@ func TestQuotaCommandsAndQueries(t *testing.T) { Target: "org_user", Limit: 10, } - err = UpdateOrgQuota(&orgCmd) + err = sqlStore.UpdateOrgQuota(context.Background(), &orgCmd) require.NoError(t, err) query = models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1} - err = GetOrgQuotaByTarget(&query) + err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(10), query.Result.Limit) }) @@ -266,11 +267,11 @@ func TestQuotaCommandsAndQueries(t *testing.T) { Target: "org_user", Limit: 5, } - err := UpdateUserQuota(&userQuotaCmd) + err := sqlStore.UpdateUserQuota(context.Background(), &userQuotaCmd) require.NoError(t, err) query := models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1} - err = GetUserQuotaByTarget(&query) + err = sqlStore.GetUserQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(5), query.Result.Limit) @@ -282,11 +283,11 @@ func TestQuotaCommandsAndQueries(t *testing.T) { Target: "org_user", Limit: 10, } - err = UpdateUserQuota(&userQuotaCmd) + err = sqlStore.UpdateUserQuota(context.Background(), &userQuotaCmd) require.NoError(t, err) query = models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1} - err = GetUserQuotaByTarget(&query) + err = sqlStore.GetUserQuotaByTarget(context.Background(), &query) require.NoError(t, err) require.Equal(t, int64(10), query.Result.Limit) }) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 122d2470a7f..03459a421f2 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -113,6 +113,7 @@ func newSQLStore(cfg *setting.Cfg, cacheService *localcache.CacheService, bus bu ss.addAlertNotificationUidByIdHandler() ss.addPreferencesQueryAndCommandHandlers() ss.addDashboardQueryAndCommandHandlers() + ss.addQuotaQueryAndCommandHandlers() // if err := ss.Reset(); err != nil { // return nil, err diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index 9ad29cdfbd6..2db8b6687f8 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -2018,7 +2018,7 @@ func TestQuota(t *testing.T) { t.Run("when quota limit exceed creating new rule should fail", func(t *testing.T) { // get existing org quota query := models.GetOrgQuotaByTargetQuery{OrgId: 1, Target: "alert_rule"} - err = sqlstore.GetOrgQuotaByTarget(&query) + err = store.GetOrgQuotaByTarget(context.Background(), &query) require.NoError(t, err) used := query.Result.Used limit := query.Result.Limit @@ -2029,7 +2029,7 @@ func TestQuota(t *testing.T) { Target: "alert_rule", Limit: used, } - err := sqlstore.UpdateOrgQuota(&orgCmd) + err := store.UpdateOrgQuota(context.Background(), &orgCmd) require.NoError(t, err) t.Cleanup(func() { @@ -2039,7 +2039,7 @@ func TestQuota(t *testing.T) { Target: "alert_rule", Limit: limit, } - err := sqlstore.UpdateOrgQuota(&orgCmd) + err := store.UpdateOrgQuota(context.Background(), &orgCmd) require.NoError(t, err) }) From fcbcfd232b59228b086d7d0b5a9e36d119523485 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Mon, 20 Sep 2021 16:05:55 +0100 Subject: [PATCH 03/76] Alerting: Move spammy log line to debug in the state manager (#39410) --- pkg/services/ngalert/state/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index bbd2f1381ad..a92bd2b5ced 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -205,7 +205,7 @@ func (st *Manager) recordMetrics() { for { select { case <-ticker.C: - st.log.Info("recording state cache metrics", "now", time.Now()) + st.log.Debug("recording state cache metrics", "now", time.Now()) st.cache.recordMetrics() case <-st.quit: st.log.Debug("stopping state cache metrics recording", "now", time.Now()) From a6fbb8d72401869c2b13ae8e8a54620bacf8fa95 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 20 Sep 2021 09:28:46 -0700 Subject: [PATCH 04/76] Live: show popup if local changes exist (#36093) * change tracker * not needed --- .../SaveDashboard/useDashboardSave.tsx | 2 ++ .../dashboard/dashgrid/DashboardGrid.tsx | 4 +++- .../features/dashboard/state/DashboardModel.ts | 16 ++++++++++++++++ .../features/live/dashboard/dashboardWatcher.ts | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index 7b607eb8d85..146aa4d45ab 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -27,6 +27,8 @@ export const useDashboardSave = (dashboard: DashboardModel) => { useEffect(() => { if (state.value) { dashboard.version = state.value.version; + dashboard.clearUnsavedChanges(); + // important that these happen before location redirect below appEvents.publish(new DashboardSavedEvent()); appEvents.emit(AppEvents.alertSuccess, ['Dashboard saved']); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index f9144f20ad6..a50735ce4b4 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -110,7 +110,9 @@ export class DashboardGrid extends PureComponent { }; onResize: ItemCallback = (layout, oldItem, newItem) => { - this.panelMap[newItem.i!].updateGridPos(newItem); + const panel = this.panelMap[newItem.i!]; + panel.updateGridPos(newItem); + panel.configRev++; // trigger change handler }; onResizeStop: ItemCallback = (layout, oldItem, newItem) => { diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 5bb6aab1c10..14b6d4d6878 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -497,6 +497,22 @@ export class DashboardModel { }); } + clearUnsavedChanges() { + for (const panel of this.panels) { + panel.configRev = 0; + } + } + + hasUnsavedChanges() { + for (const panel of this.panels) { + if (panel.hasChanged) { + console.log('Panel has changed', panel); + return true; + } + } + return false; + } + cleanUpRepeats() { if (this.isSnapshotTruthy() || !this.hasVariables()) { return; diff --git a/public/app/features/live/dashboard/dashboardWatcher.ts b/public/app/features/live/dashboard/dashboardWatcher.ts index 4eaa1d85b9e..a49de120d17 100644 --- a/public/app/features/live/dashboard/dashboardWatcher.ts +++ b/public/app/features/live/dashboard/dashboardWatcher.ts @@ -116,7 +116,7 @@ class DashboardWatcher { return; } - const showPopup = this.editing; // || changeTracker.hasChanges(); + const showPopup = this.editing || dash.hasUnsavedChanges(); if (action === DashboardEventAction.Saved) { if (showPopup) { From ad3c7529b04385a40de522b13ee1774943a30f2e Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 20 Sep 2021 12:25:56 -0700 Subject: [PATCH 05/76] Schema: unify bar/point/etc visibility modes (#39378) --- packages/grafana-schema/src/schema/graph.cue | 5 ++--- packages/grafana-schema/src/schema/graph.gen.ts | 16 +++------------- .../src/components/GraphNG/utils.test.ts | 12 ++++++------ .../src/components/Sparkline/Sparkline.tsx | 6 +++--- .../src/components/TimeSeries/utils.ts | 6 +++--- .../grafana-ui/src/components/uPlot/config.ts | 10 +++++----- .../uPlot/config/UPlotConfigBuilder.test.ts | 10 +++++----- .../uPlot/config/UPlotSeriesBuilder.ts | 8 ++++---- packages/grafana-ui/src/schema.ts | 4 ++-- public/app/plugins/panel/barchart/bars.ts | 14 ++++---------- public/app/plugins/panel/barchart/models.cue | 2 +- public/app/plugins/panel/barchart/module.tsx | 10 +++++----- public/app/plugins/panel/barchart/types.ts | 4 ++-- public/app/plugins/panel/barchart/utils.test.ts | 6 +++--- .../panel/state-timeline/TimelineChart.tsx | 4 ++-- .../app/plugins/panel/state-timeline/models.cue | 2 +- .../app/plugins/panel/state-timeline/module.tsx | 8 ++++---- .../app/plugins/panel/state-timeline/timeline.ts | 8 ++++---- public/app/plugins/panel/state-timeline/types.ts | 6 +++--- .../app/plugins/panel/status-history/models.cue | 2 +- .../app/plugins/panel/status-history/module.tsx | 10 +++++----- public/app/plugins/panel/status-history/types.ts | 4 ++-- public/app/plugins/panel/timeseries/config.ts | 4 ++-- .../app/plugins/panel/timeseries/migrations.ts | 8 ++++---- 24 files changed, 76 insertions(+), 93 deletions(-) diff --git a/packages/grafana-schema/src/schema/graph.cue b/packages/grafana-schema/src/schema/graph.cue index 3a1d9f13d1f..c015e954f00 100644 --- a/packages/grafana-schema/src/schema/graph.cue +++ b/packages/grafana-schema/src/schema/graph.cue @@ -1,13 +1,12 @@ package schema AxisPlacement: "auto" | "top" | "right" | "bottom" | "left" | "hidden" @cuetsy(kind="enum") -PointVisibility: "auto" | "never" | "always" @cuetsy(kind="enum") +VisibilityMode: "auto" | "never" | "always" @cuetsy(kind="enum") DrawStyle: "line" | "bars" | "points" @cuetsy(kind="enum") LineInterpolation: "linear" | "smooth" | "stepBefore" | "stepAfter" @cuetsy(kind="enum") ScaleDistribution: "linear" | "log" @cuetsy(kind="enum") GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(kind="enum") StackingMode: "none" | "normal" | "percent" @cuetsy(kind="enum") -BarValueVisibility: "auto" | "never" | "always" @cuetsy(kind="enum") BarAlignment: -1 | 0 | 1 @cuetsy(kind="enum",memberNames="Before|Center|After") ScaleOrientation: 0 | 1 @cuetsy(kind="enum",memberNames="Horizontal|Vertical") ScaleDirection: 1 | 1 | -1 | -1 @cuetsy(kind="enum",memberNames="Up|Right|Down|Left") @@ -33,7 +32,7 @@ FillConfig: { fillBelowTo?: string } @cuetsy(kind="interface") PointsConfig: { - showPoints?: PointVisibility + showPoints?: VisibilityMode pointSize?: number pointColor?: string pointSymbol?: string diff --git a/packages/grafana-schema/src/schema/graph.gen.ts b/packages/grafana-schema/src/schema/graph.gen.ts index 081fb369005..fc38add7062 100644 --- a/packages/grafana-schema/src/schema/graph.gen.ts +++ b/packages/grafana-schema/src/schema/graph.gen.ts @@ -11,9 +11,9 @@ export enum AxisPlacement { Right = 'right', Top = 'top', } -export enum PointVisibility { - Always = 'always', +export enum VisibilityMode { Auto = 'auto', + Always = 'always', Never = 'never', } export enum GraphDrawStyle { @@ -45,12 +45,11 @@ export interface LineStyle { dash?: number[]; fill?: 'solid' | 'dash' | 'dot' | 'square'; } - export interface PointsConfig { pointColor?: string; pointSize?: number; pointSymbol?: string; - showPoints?: PointVisibility; + showPoints?: VisibilityMode; } export interface ScaleDistributionConfig { log?: number; @@ -71,15 +70,6 @@ export enum BarAlignment { After = 1, } -/** - * @alpha - */ -export enum BarValueVisibility { - Auto = 'auto', - Never = 'never', - Always = 'always', -} - /** * @alpha */ diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/components/GraphNG/utils.test.ts index 49f99787583..7552cd1974f 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.test.ts @@ -18,7 +18,7 @@ import { GraphFieldConfig, GraphGradientMode, LineInterpolation, - PointVisibility, + VisibilityMode, StackingMode, } from '@grafana/schema'; @@ -48,7 +48,7 @@ function mockDataFrame() { spanNulls: false, fillColor: '#ff0000', fillOpacity: 0.1, - showPoints: PointVisibility.Always, + showPoints: VisibilityMode.Always, stacking: { group: 'A', mode: StackingMode.Normal, @@ -72,7 +72,7 @@ function mockDataFrame() { barAlignment: BarAlignment.Before, fillColor: '#ff0000', fillOpacity: 0.1, - showPoints: PointVisibility.Always, + showPoints: VisibilityMode.Always, stacking: { group: 'A', mode: StackingMode.Normal, @@ -96,7 +96,7 @@ function mockDataFrame() { spanNulls: false, fillColor: '#ff0000', fillOpacity: 0.1, - showPoints: PointVisibility.Always, + showPoints: VisibilityMode.Always, stacking: { group: 'B', mode: StackingMode.Normal, @@ -119,7 +119,7 @@ function mockDataFrame() { barAlignment: BarAlignment.Before, fillColor: '#ff0000', fillOpacity: 0.1, - showPoints: PointVisibility.Always, + showPoints: VisibilityMode.Always, stacking: { group: 'B', mode: StackingMode.Normal, @@ -142,7 +142,7 @@ function mockDataFrame() { barAlignment: BarAlignment.Before, fillColor: '#ff0000', fillOpacity: 0.1, - showPoints: PointVisibility.Always, + showPoints: VisibilityMode.Always, stacking: { group: 'B', mode: StackingMode.None, diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index 8e36177fb0d..f6c16aec35f 100755 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -13,7 +13,7 @@ import { AxisPlacement, GraphDrawStyle, GraphFieldConfig, - PointVisibility, + VisibilityMode, ScaleDirection, ScaleOrientation, } from '@grafana/schema'; @@ -39,7 +39,7 @@ interface State { const defaultConfig: GraphFieldConfig = { drawStyle: GraphDrawStyle.Line, - showPoints: PointVisibility.Auto, + showPoints: VisibilityMode.Auto, axisPlacement: AxisPlacement.Hidden, }; @@ -163,7 +163,7 @@ export class Sparkline extends PureComponent { const colorMode = getFieldColorModeForField(field); const seriesColor = colorMode.getCalculator(field, theme)(0, 0); const pointsMode = - customConfig.drawStyle === GraphDrawStyle.Points ? PointVisibility.Always : customConfig.showPoints; + customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; builder.addSeries({ pxAlign: false, diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index ac66bbc39d6..0ccbdb7638e 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -20,7 +20,7 @@ import { GraphDrawStyle, GraphFieldConfig, GraphTresholdsStyleMode, - PointVisibility, + VisibilityMode, ScaleDirection, ScaleOrientation, } from '@grafana/schema'; @@ -31,7 +31,7 @@ const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); const defaultConfig: GraphFieldConfig = { drawStyle: GraphDrawStyle.Line, - showPoints: PointVisibility.Auto, + showPoints: VisibilityMode.Auto, axisPlacement: AxisPlacement.Auto, }; @@ -154,7 +154,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor } const showPoints = - customConfig.drawStyle === GraphDrawStyle.Points ? PointVisibility.Always : customConfig.showPoints; + customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; let pointsFilter: uPlot.Series.Points.Filter = () => null; diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index 1dd825cf396..cb094c5eb2c 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -6,7 +6,7 @@ import { GraphGradientMode, GraphTresholdsStyleMode, LineInterpolation, - PointVisibility, + VisibilityMode, StackingMode, } from '@grafana/schema'; @@ -34,10 +34,10 @@ export const graphFieldOptions = { ] as Array>, showPoints: [ - { label: 'Auto', value: PointVisibility.Auto, description: 'Show points when the density is low' }, - { label: 'Always', value: PointVisibility.Always }, - { label: 'Never', value: PointVisibility.Never }, - ] as Array>, + { label: 'Auto', value: VisibilityMode.Auto, description: 'Show points when the density is low' }, + { label: 'Always', value: VisibilityMode.Always }, + { label: 'Never', value: VisibilityMode.Never }, + ] as Array>, axisPlacement: [ { label: 'Auto', value: AxisPlacement.Auto, description: 'First field on the left, everything else on the right' }, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index 57fc787fce7..50b93c34527 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -5,7 +5,7 @@ import { GraphGradientMode, AxisPlacement, GraphDrawStyle, - PointVisibility, + VisibilityMode, ScaleOrientation, ScaleDirection, GraphTresholdsStyleMode, @@ -479,7 +479,7 @@ describe('UPlotConfigBuilder', () => { scaleKey: 'scale-x', fillOpacity: 50, gradientMode: GraphGradientMode.Opacity, - showPoints: PointVisibility.Auto, + showPoints: VisibilityMode.Auto, pointSize: 5, lineColor: '#0000ff', lineWidth: 1, @@ -544,7 +544,7 @@ describe('UPlotConfigBuilder', () => { scaleKey: 'scale-x', fillOpacity: 50, gradientMode: GraphGradientMode.Opacity, - showPoints: PointVisibility.Auto, + showPoints: VisibilityMode.Auto, lineColor: '#0000ff', lineWidth: 1, spanNulls: false, @@ -555,7 +555,7 @@ describe('UPlotConfigBuilder', () => { scaleKey: 'scale-x', fillOpacity: 50, gradientMode: GraphGradientMode.Opacity, - showPoints: PointVisibility.Auto, + showPoints: VisibilityMode.Auto, pointSize: 5, lineColor: '#00ff00', lineWidth: 1, @@ -568,7 +568,7 @@ describe('UPlotConfigBuilder', () => { scaleKey: 'scale-x', fillOpacity: 50, gradientMode: GraphGradientMode.Opacity, - showPoints: PointVisibility.Auto, + showPoints: VisibilityMode.Auto, pointSize: 5, lineColor: '#ff0000', lineWidth: 1, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index c7cd5356d8f..c02e6c4cb84 100755 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -17,7 +17,7 @@ import { LineConfig, LineInterpolation, PointsConfig, - PointVisibility, + VisibilityMode, } from '@grafana/schema'; import { PlotConfigBuilder } from '../types'; import { getHueGradientFn, getOpacityGradientFn, getScaleGradientFn } from './gradientFills'; @@ -118,13 +118,13 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { if (drawStyle === GraphDrawStyle.Points) { pointsConfig.points!.show = true; } else { - if (showPoints === PointVisibility.Auto) { + if (showPoints === VisibilityMode.Auto) { if (drawStyle === GraphDrawStyle.Bars) { pointsConfig.points!.show = false; } - } else if (showPoints === PointVisibility.Never) { + } else if (showPoints === VisibilityMode.Never) { pointsConfig.points!.show = false; - } else if (showPoints === PointVisibility.Always) { + } else if (showPoints === VisibilityMode.Always) { pointsConfig.points!.show = true; } } diff --git a/packages/grafana-ui/src/schema.ts b/packages/grafana-ui/src/schema.ts index 6d86f1575bd..a9e03c4faec 100644 --- a/packages/grafana-ui/src/schema.ts +++ b/packages/grafana-ui/src/schema.ts @@ -7,7 +7,7 @@ export { GraphDrawStyle as DrawStyle, // All exports AxisPlacement, - PointVisibility, + VisibilityMode as PointVisibility, LineInterpolation, ScaleDistribution, GraphGradientMode, @@ -16,7 +16,7 @@ export { ScaleDistributionConfig, HideSeriesConfig, BarAlignment, - BarValueVisibility, + VisibilityMode as BarValueVisibility, ScaleOrientation, ScaleDirection, LineConfig, diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index 257444bec29..ffe1f40f933 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -3,13 +3,7 @@ import { pointWithin, Quadtree, Rect } from './quadtree'; import { distribute, SPACE_BETWEEN } from './distribute'; import { DataFrame, GrafanaTheme2 } from '@grafana/data'; import { calculateFontSize, PlotTooltipInterpolator } from '@grafana/ui'; -import { - StackingMode, - BarValueVisibility, - ScaleDirection, - ScaleOrientation, - VizTextDisplayOptions, -} from '@grafana/schema'; +import { StackingMode, VisibilityMode, ScaleDirection, ScaleOrientation, VizTextDisplayOptions } from '@grafana/schema'; import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; const groupDistr = SPACE_BETWEEN; @@ -39,7 +33,7 @@ export interface BarsOptions { xDir: ScaleDirection; groupWidth: number; barWidth: number; - showValue: BarValueVisibility; + showValue: VisibilityMode; stacking: StackingMode; rawValue: (seriesIdx: number, valueIdx: number) => number | null; formatValue: (seriesIdx: number, value: any) => string; @@ -199,7 +193,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { // uPlot hook to draw the labels on the bar chart. const draw = (u: uPlot) => { - if (showValue === BarValueVisibility.Never) { + if (showValue === VisibilityMode.Never) { return; } // pre-cache formatted labels @@ -228,7 +222,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { ) ); - if (fontSize < VALUE_MIN_FONT_SIZE && showValue !== BarValueVisibility.Always) { + if (fontSize < VALUE_MIN_FONT_SIZE && showValue !== VisibilityMode.Always) { return; } } diff --git a/public/app/plugins/panel/barchart/models.cue b/public/app/plugins/panel/barchart/models.cue index c74d081ef30..c39a85e0ce9 100644 --- a/public/app/plugins/panel/barchart/models.cue +++ b/public/app/plugins/panel/barchart/models.cue @@ -29,7 +29,7 @@ Panel: { orientation: ui.VizOrientation // TODO this default is a guess based on common devenv values stacking: ui.StackingMode | *"none" - showValue: ui.BarValueVisibility + showValue: ui.VisibilityMode barWidth: number groupWidth: number } diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 3baf5f4a813..9dd8271b548 100755 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -7,7 +7,7 @@ import { VizOrientation, } from '@grafana/data'; import { BarChartPanel } from './BarChartPanel'; -import { StackingMode, BarValueVisibility } from '@grafana/schema'; +import { StackingMode, VisibilityMode } from '@grafana/schema'; import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui'; import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from 'app/plugins/panel/barchart/types'; @@ -80,12 +80,12 @@ export const plugin = new PanelPlugin(BarC name: 'Show values', settings: { options: [ - { value: BarValueVisibility.Auto, label: 'Auto' }, - { value: BarValueVisibility.Always, label: 'Always' }, - { value: BarValueVisibility.Never, label: 'Never' }, + { value: VisibilityMode.Auto, label: 'Auto' }, + { value: VisibilityMode.Always, label: 'Always' }, + { value: VisibilityMode.Never, label: 'Never' }, ], }, - defaultValue: BarValueVisibility.Auto, + defaultValue: VisibilityMode.Auto, }) .addRadio({ path: 'stacking', diff --git a/public/app/plugins/panel/barchart/types.ts b/public/app/plugins/panel/barchart/types.ts index ff8a6476595..4d12c1b960e 100644 --- a/public/app/plugins/panel/barchart/types.ts +++ b/public/app/plugins/panel/barchart/types.ts @@ -3,7 +3,7 @@ import { OptionsWithTextFormatting, OptionsWithTooltip, AxisConfig, - BarValueVisibility, + VisibilityMode, GraphGradientMode, HideableFieldConfig, StackingMode, @@ -16,7 +16,7 @@ import { VizOrientation } from '@grafana/data'; export interface BarChartOptions extends OptionsWithLegend, OptionsWithTooltip, OptionsWithTextFormatting { orientation: VizOrientation; stacking: StackingMode; - showValue: BarValueVisibility; + showValue: VisibilityMode; barWidth: number; groupWidth: number; rawValue: (seriesIdx: number, valueIdx: number) => number; diff --git a/public/app/plugins/panel/barchart/utils.test.ts b/public/app/plugins/panel/barchart/utils.test.ts index 4df56dd2111..0b1333f4f22 100644 --- a/public/app/plugins/panel/barchart/utils.test.ts +++ b/public/app/plugins/panel/barchart/utils.test.ts @@ -2,7 +2,7 @@ import { prepareGraphableFrames, preparePlotConfigBuilder, preparePlotFrame } fr import { LegendDisplayMode, TooltipDisplayMode, - BarValueVisibility, + VisibilityMode, GraphGradientMode, StackingMode, } from '@grafana/schema'; @@ -81,7 +81,7 @@ describe('BarChart utils', () => { orientation: VizOrientation.Auto, groupWidth: 20, barWidth: 2, - showValue: BarValueVisibility.Always, + showValue: VisibilityMode.Always, legend: { displayMode: LegendDisplayMode.List, placement: 'bottom', @@ -111,7 +111,7 @@ describe('BarChart utils', () => { expect(result).toMatchSnapshot(); }); - it.each([BarValueVisibility.Always, BarValueVisibility.Auto])('value visibility', (v) => { + it.each([VisibilityMode.Always, VisibilityMode.Auto])('value visibility', (v) => { expect( preparePlotConfigBuilder({ ...config, diff --git a/public/app/plugins/panel/state-timeline/TimelineChart.tsx b/public/app/plugins/panel/state-timeline/TimelineChart.tsx index 22444eb9d73..0035b989b1c 100755 --- a/public/app/plugins/panel/state-timeline/TimelineChart.tsx +++ b/public/app/plugins/panel/state-timeline/TimelineChart.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { LegendDisplayMode, BarValueVisibility } from '@grafana/schema'; +import { LegendDisplayMode, VisibilityMode } from '@grafana/schema'; import { PanelContext, PanelContextRoot, @@ -22,7 +22,7 @@ export interface TimelineProps Omit { mode: TimelineMode; rowHeight: number; - showValue: BarValueVisibility; + showValue: VisibilityMode; alignValue?: TimelineValueAlignment; colWidth?: number; legendItems?: VizLegendItem[]; diff --git a/public/app/plugins/panel/state-timeline/models.cue b/public/app/plugins/panel/state-timeline/models.cue index 9dd522f793d..5acb0209d2b 100644 --- a/public/app/plugins/panel/state-timeline/models.cue +++ b/public/app/plugins/panel/state-timeline/models.cue @@ -29,7 +29,7 @@ Panel: { mode?: #TimelineMode ui.OptionsWithLegend ui.OptionsWithTooltip - showValue: ui.BarValueVisibility | *"auto" + showValue: ui.VisibilityMode | *"auto" rowHeight: number | *0.9 colWidth?: number mergeValues?: bool | *true diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index 793d0c2f9e9..b4a2791c29b 100755 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -1,7 +1,7 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; import { StateTimelinePanel } from './StateTimelinePanel'; import { TimelineOptions, TimelineFieldConfig, defaultPanelOptions, defaultTimelineFieldConfig } from './types'; -import { BarValueVisibility } from '@grafana/schema'; +import { VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { timelinePanelChangedHandler } from './migrations'; @@ -54,9 +54,9 @@ export const plugin = new PanelPlugin(Stat name: 'Show values', settings: { options: [ - { value: BarValueVisibility.Auto, label: 'Auto' }, - { value: BarValueVisibility.Always, label: 'Always' }, - { value: BarValueVisibility.Never, label: 'Never' }, + { value: VisibilityMode.Auto, label: 'Auto' }, + { value: VisibilityMode.Always, label: 'Always' }, + { value: VisibilityMode.Never, label: 'Never' }, ], }, defaultValue: defaultPanelOptions.showValue, diff --git a/public/app/plugins/panel/state-timeline/timeline.ts b/public/app/plugins/panel/state-timeline/timeline.ts index a9d3f525bd4..b82ca170a22 100644 --- a/public/app/plugins/panel/state-timeline/timeline.ts +++ b/public/app/plugins/panel/state-timeline/timeline.ts @@ -4,7 +4,7 @@ import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute'; import { TimelineFieldConfig, TimelineMode, TimelineValueAlignment } from './types'; import { GrafanaTheme2, TimeRange } from '@grafana/data'; -import { BarValueVisibility } from '@grafana/schema'; +import { VisibilityMode } from '@grafana/schema'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; const { round, min, ceil } = Math; @@ -40,7 +40,7 @@ export interface TimelineCoreOptions { rowHeight: number; colWidth?: number; theme: GrafanaTheme2; - showValue: BarValueVisibility; + showValue: VisibilityMode; isDiscrete: (seriesIdx: number) => boolean; getValueColor: (seriesIdx: number, value: any) => string; label: (seriesIdx: number) => string; @@ -288,7 +288,7 @@ export function getConfig(opts: TimelineCoreOptions) { }; const drawPoints: Series.Points.Show = - formatValue == null || showValue === BarValueVisibility.Never + formatValue == null || showValue === VisibilityMode.Never ? false : (u, sidx, i0, i1) => { u.ctx.save(); @@ -312,7 +312,7 @@ export function getConfig(opts: TimelineCoreOptions) { const boxRect = boxRectsBySeries[sidx - 1][ix]; // Todo refine this to better know when to not render text (when values do not fit) - if (!boxRect || (showValue === BarValueVisibility.Auto && boxRect.w < 25)) { + if (!boxRect || (showValue === VisibilityMode.Auto && boxRect.w < 25)) { continue; } diff --git a/public/app/plugins/panel/state-timeline/types.ts b/public/app/plugins/panel/state-timeline/types.ts index 909cb58a55a..b9a8d011eb9 100644 --- a/public/app/plugins/panel/state-timeline/types.ts +++ b/public/app/plugins/panel/state-timeline/types.ts @@ -1,4 +1,4 @@ -import { OptionsWithTooltip, OptionsWithLegend, HideableFieldConfig, BarValueVisibility } from '@grafana/schema'; +import { OptionsWithTooltip, OptionsWithLegend, HideableFieldConfig, VisibilityMode } from '@grafana/schema'; /** * @alpha @@ -6,7 +6,7 @@ import { OptionsWithTooltip, OptionsWithLegend, HideableFieldConfig, BarValueVis export interface TimelineOptions extends OptionsWithLegend, OptionsWithTooltip { mode: TimelineMode; // not in the saved model! - showValue: BarValueVisibility; + showValue: VisibilityMode; rowHeight: number; // only used for "samples" mode (status-history) @@ -31,7 +31,7 @@ export interface TimelineFieldConfig extends HideableFieldConfig { * @alpha */ export const defaultPanelOptions: Partial = { - showValue: BarValueVisibility.Auto, + showValue: VisibilityMode.Auto, alignValue: 'left', mergeValues: true, rowHeight: 0.9, diff --git a/public/app/plugins/panel/status-history/models.cue b/public/app/plugins/panel/status-history/models.cue index 50490b29e56..96250734e6a 100644 --- a/public/app/plugins/panel/status-history/models.cue +++ b/public/app/plugins/panel/status-history/models.cue @@ -25,7 +25,7 @@ Panel: { PanelOptions: { ui.OptionsWithLegend ui.OptionsWithTooltip - showValue: ui.BarValueVisibility + showValue: ui.VisibilityMode rowHeight: number colWidth?: number alignValue: "center" | *"left" | "right" diff --git a/public/app/plugins/panel/status-history/module.tsx b/public/app/plugins/panel/status-history/module.tsx index 505a241e846..76c02ad2c6a 100755 --- a/public/app/plugins/panel/status-history/module.tsx +++ b/public/app/plugins/panel/status-history/module.tsx @@ -1,7 +1,7 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; import { StatusHistoryPanel } from './StatusHistoryPanel'; import { StatusPanelOptions, StatusFieldConfig, defaultStatusFieldConfig } from './types'; -import { BarValueVisibility } from '@grafana/schema'; +import { VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; export const plugin = new PanelPlugin(StatusHistoryPanel) @@ -47,12 +47,12 @@ export const plugin = new PanelPlugin(Sta name: 'Show values', settings: { options: [ - { value: BarValueVisibility.Auto, label: 'Auto' }, - { value: BarValueVisibility.Always, label: 'Always' }, - { value: BarValueVisibility.Never, label: 'Never' }, + { value: VisibilityMode.Auto, label: 'Auto' }, + { value: VisibilityMode.Always, label: 'Always' }, + { value: VisibilityMode.Never, label: 'Never' }, ], }, - defaultValue: BarValueVisibility.Auto, + defaultValue: VisibilityMode.Auto, }) .addSliderInput({ path: 'rowHeight', diff --git a/public/app/plugins/panel/status-history/types.ts b/public/app/plugins/panel/status-history/types.ts index ed96f94e634..0e4b4b65277 100644 --- a/public/app/plugins/panel/status-history/types.ts +++ b/public/app/plugins/panel/status-history/types.ts @@ -1,10 +1,10 @@ -import { HideableFieldConfig, BarValueVisibility, OptionsWithTooltip, OptionsWithLegend } from '@grafana/schema'; +import { HideableFieldConfig, VisibilityMode, OptionsWithTooltip, OptionsWithLegend } from '@grafana/schema'; /** * @alpha */ export interface StatusPanelOptions extends OptionsWithTooltip, OptionsWithLegend { - showValue: BarValueVisibility; + showValue: VisibilityMode; rowHeight: number; colWidth?: number; } diff --git a/public/app/plugins/panel/timeseries/config.ts b/public/app/plugins/panel/timeseries/config.ts index f9376ce4d68..ed8ea4a8da6 100644 --- a/public/app/plugins/panel/timeseries/config.ts +++ b/public/app/plugins/panel/timeseries/config.ts @@ -13,7 +13,7 @@ import { GraphGradientMode, LineInterpolation, LineStyle, - PointVisibility, + VisibilityMode, StackingMode, GraphTresholdsStyleMode, } from '@grafana/schema'; @@ -174,7 +174,7 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption max: 40, step: 1, }, - showIf: (c) => c.showPoints !== PointVisibility.Never || c.drawStyle === GraphDrawStyle.Points, + showIf: (c) => c.showPoints !== VisibilityMode.Never || c.drawStyle === GraphDrawStyle.Points, }); commonOptionsBuilder.addStackingConfig(builder, cfg.stacking, categoryStyles); diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts index 2f25e260a67..8e80f853389 100644 --- a/public/app/plugins/panel/timeseries/migrations.ts +++ b/public/app/plugins/panel/timeseries/migrations.ts @@ -22,7 +22,7 @@ import { GraphTresholdsStyleMode, LineInterpolation, LineStyle, - PointVisibility, + VisibilityMode, ScaleDistribution, StackingMode, } from '@grafana/schema'; @@ -166,7 +166,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour case 'points': rule.properties.push({ id: 'custom.showPoints', - value: v ? PointVisibility.Always : PointVisibility.Never, + value: v ? VisibilityMode.Always : VisibilityMode.Never, }); break; case 'bars': @@ -260,13 +260,13 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour graph.drawStyle = angular.bars ? GraphDrawStyle.Bars : angular.lines ? GraphDrawStyle.Line : GraphDrawStyle.Points; if (angular.points) { - graph.showPoints = PointVisibility.Always; + graph.showPoints = VisibilityMode.Always; if (isNumber(angular.pointradius)) { graph.pointSize = 2 + angular.pointradius * 2; } } else if (graph.drawStyle !== GraphDrawStyle.Points) { - graph.showPoints = PointVisibility.Never; + graph.showPoints = VisibilityMode.Never; } graph.lineWidth = angular.linewidth; From 1781c8ec7d7e4356e81ef4fe622acbc94b194970 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Mon, 20 Sep 2021 16:21:59 -0400 Subject: [PATCH 06/76] Chore: Add go-redis v8 dependency (#39442) * adds redis v8 client dependency * remove go-redis v5 dependency --- go.mod | 3 ++- go.sum | 19 ++++++++++++++----- pkg/infra/remotecache/redis_storage.go | 9 +++++---- pkg/infra/remotecache/redis_storage_test.go | 2 +- pkg/services/live/live.go | 4 ++-- .../live/managedstream/cache_redis.go | 15 +++++++++------ .../live/managedstream/cache_redis_test.go | 2 +- 7 files changed, 34 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index d5c55101620..4addbcec2d7 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/go-kit/kit v0.11.0 github.com/go-macaron/binding v0.0.0-20190806013118-0b4f37bab25b github.com/go-openapi/strfmt v0.20.1 + github.com/go-redis/redis/v8 v8.11.3 github.com/go-sourcemap/sourcemap v2.1.3+incompatible github.com/go-sql-driver/mysql v1.6.0 github.com/go-stack/stack v1.8.0 @@ -112,7 +113,6 @@ require ( gopkg.in/ldap.v3 v3.1.0 gopkg.in/macaron.v1 v1.4.0 gopkg.in/mail.v2 v2.3.1 - gopkg.in/redis.v5 v5.2.9 gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b @@ -141,6 +141,7 @@ require ( github.com/cockroachdb/apd/v2 v2.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/deepmap/oapi-codegen v1.3.13 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect diff --git a/go.sum b/go.sum index 97ab66c9002..80e1507facd 100644 --- a/go.sum +++ b/go.sum @@ -461,6 +461,7 @@ github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/ github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -759,9 +760,12 @@ github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3h github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v8 v8.0.0-beta.10.0.20200905143926-df7fe4e2ce72/go.mod h1:CJP1ZIHwhosNYwIdaHPZK9vHsM3+roNBaZ7U9Of1DXc= github.com/go-redis/redis/v8 v8.2.3/go.mod h1:ysgGY09J/QeDYbu3HikWEIPCwaeOkuNoTgKayTEaEOw= +github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8= +github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= @@ -773,6 +777,7 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= @@ -1567,8 +1572,9 @@ github.com/newrelic/newrelic-telemetry-sdk-go v0.2.0/go.mod h1:G9MqE/cHGv3Hx3qpY github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2/go.mod h1:TLb2Sg7HQcgGdloNxkrmtgDNR9uVYF3lfdFIN4Ro6Sk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsqio/go-nsq v1.0.7/go.mod h1:XP5zaUs3pqf+Q71EqUJs3HYfBIqfK6G83WQMdNN+Ito= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= @@ -1594,8 +1600,9 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -1604,8 +1611,9 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/openconfig/gnmi v0.0.0-20180912164834-33a1865c3029/go.mod h1:t+O9It+LKzfOAhKTT5O0ehDix+MTqbtT0T9t+7zzOvc= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -2356,6 +2364,7 @@ golang.org/x/net v0.0.0-20210324051636-2c4c8ecb7826/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -2500,6 +2509,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2652,6 +2662,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201119054027-25dc3e1ccc3c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201226215659-b1c90890d22a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -2898,8 +2909,6 @@ gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/olivere/elastic.v5 v5.0.70/go.mod h1:FylZT6jQWtfHsicejzOm3jIMVPOAksa80i3o+6qtQRk= -gopkg.in/redis.v5 v5.2.9 h1:MNZYOLPomQzZMfpN3ZtD1uyJ2IDonTTlxYiV/pEApiw= -gopkg.in/redis.v5 v5.2.9/go.mod h1:6gtv0/+A4iM08kdRfocWYB3bLX2tebpNtfKlFT6H4mY= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go index 59d751c3db0..d0b67995f3f 100644 --- a/pkg/infra/remotecache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -1,15 +1,16 @@ package remotecache import ( + "context" "crypto/tls" "fmt" "strconv" "strings" "time" + "github.com/go-redis/redis/v8" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" - redis "gopkg.in/redis.v5" ) const redisCacheType = "redis" @@ -91,13 +92,13 @@ func (s *redisStorage) Set(key string, val interface{}, expires time.Duration) e if err != nil { return err } - status := s.c.Set(key, string(value), expires) + status := s.c.Set(context.TODO(), key, string(value), expires) return status.Err() } // Get gets value by given key in session. func (s *redisStorage) Get(key string) (interface{}, error) { - v := s.c.Get(key) + v := s.c.Get(context.TODO(), key) item := &cachedItem{} err := decodeGob([]byte(v.Val()), item) @@ -113,6 +114,6 @@ func (s *redisStorage) Get(key string) (interface{}, error) { // Delete delete a key from session. func (s *redisStorage) Delete(key string) error { - cmd := s.c.Del(key) + cmd := s.c.Del(context.TODO(), key) return cmd.Err() } diff --git a/pkg/infra/remotecache/redis_storage_test.go b/pkg/infra/remotecache/redis_storage_test.go index 0ba9dcc907e..32138431f21 100644 --- a/pkg/infra/remotecache/redis_storage_test.go +++ b/pkg/infra/remotecache/redis_storage_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" + "github.com/go-redis/redis/v8" "github.com/stretchr/testify/assert" - redis "gopkg.in/redis.v5" ) func Test_parseRedisConnStr(t *testing.T) { diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index e683ded4074..5dc17b394b1 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -37,11 +37,11 @@ import ( "github.com/grafana/grafana/pkg/util" "github.com/centrifugal/centrifuge" + "github.com/go-redis/redis/v8" "github.com/gobwas/glob" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/live" "gopkg.in/macaron.v1" - "gopkg.in/redis.v5" ) var ( @@ -146,7 +146,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r redisClient := redis.NewClient(&redis.Options{ Addr: g.Cfg.LiveHAEngineAddress, }) - cmd := redisClient.Ping() + cmd := redisClient.Ping(context.TODO()) if _, err := cmd.Result(); err != nil { return nil, fmt.Errorf("error pinging Redis: %v", err) } diff --git a/pkg/services/live/managedstream/cache_redis.go b/pkg/services/live/managedstream/cache_redis.go index e080a59f275..2523c8b2747 100644 --- a/pkg/services/live/managedstream/cache_redis.go +++ b/pkg/services/live/managedstream/cache_redis.go @@ -1,6 +1,7 @@ package managedstream import ( + "context" "encoding/json" "errors" "sync" @@ -8,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/services/live/orgchannel" + "github.com/go-redis/redis/v8" "github.com/grafana/grafana-plugin-sdk-go/data" - "gopkg.in/redis.v5" ) // RedisFrameCache ... @@ -43,7 +44,7 @@ func (c *RedisFrameCache) GetActiveChannels(orgID int64) (map[string]json.RawMes func (c *RedisFrameCache) GetFrame(orgID int64, channel string) (json.RawMessage, bool, error) { key := getCacheKey(orgchannel.PrependOrgID(orgID, channel)) - cmd := c.redisClient.HGetAll(key) + cmd := c.redisClient.HGetAll(context.TODO(), key) result, err := cmd.Result() if err != nil { return nil, false, err @@ -73,14 +74,16 @@ func (c *RedisFrameCache) Update(orgID int64, channel string, jsonFrame data.Fra pipe := c.redisClient.TxPipeline() defer func() { _ = pipe.Close() }() - pipe.HGetAll(key) - pipe.HMSet(key, map[string]string{ + ctx := context.TODO() + + pipe.HGetAll(ctx, key) + pipe.HMSet(ctx, key, map[string]string{ "schema": stringSchema, "frame": string(jsonFrame.Bytes(data.IncludeAll)), }) - pipe.Expire(key, frameCacheTTL) + pipe.Expire(ctx, key, frameCacheTTL) - replies, err := pipe.Exec() + replies, err := pipe.Exec(ctx) if err != nil { return false, err } diff --git a/pkg/services/live/managedstream/cache_redis_test.go b/pkg/services/live/managedstream/cache_redis_test.go index a9183e69936..df650ded044 100644 --- a/pkg/services/live/managedstream/cache_redis_test.go +++ b/pkg/services/live/managedstream/cache_redis_test.go @@ -6,8 +6,8 @@ package managedstream import ( "testing" + "github.com/go-redis/redis/v8" "github.com/stretchr/testify/require" - "gopkg.in/redis.v5" ) func TestRedisCacheStorage(t *testing.T) { From d03f75726b8135f33e6c284c00dc8814f875f77e Mon Sep 17 00:00:00 2001 From: Nathan Rodman Date: Mon, 20 Sep 2021 15:32:25 -0700 Subject: [PATCH 07/76] Alerting: Add filtering for Silences (#39109) * Add filtering for Silences page * Add tests Silences and SilenceEditor * pr feedback: add field validation and test refactor * Add test for checking content * fix overflow for validation error message * increase login threshold for pa11y * Make silence filter state its own type and function --- .pa11yci-pr.conf.js | 2 +- .../alerting/unified/Silences.test.tsx | 247 ++++++++++++++++++ .../components/silences/MatchersField.tsx | 4 +- .../components/silences/SilencePeriod.tsx | 1 + .../components/silences/SilenceTableRow.tsx | 4 +- .../components/silences/SilencesEditor.tsx | 22 +- .../components/silences/SilencesFilter.tsx | 109 ++++++++ .../components/silences/SilencesTable.tsx | 149 +++++++---- public/app/features/alerting/unified/mocks.ts | 19 +- .../features/alerting/unified/utils/misc.ts | 12 +- public/app/types/unified-alerting.ts | 6 + 11 files changed, 510 insertions(+), 65 deletions(-) create mode 100644 public/app/features/alerting/unified/Silences.test.tsx create mode 100644 public/app/features/alerting/unified/components/silences/SilencesFilter.tsx diff --git a/.pa11yci-pr.conf.js b/.pa11yci-pr.conf.js index 9e5f55daaa2..fde20070e96 100644 --- a/.pa11yci-pr.conf.js +++ b/.pa11yci-pr.conf.js @@ -16,7 +16,7 @@ var config = { "click element button[aria-label='Login button']", "wait for element [aria-label='Skip change password button'] to be visible", ], - threshold: 2, + threshold: 3, }, { url: '${HOST}/?orgId=1', diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx new file mode 100644 index 00000000000..4b8280c9c65 --- /dev/null +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -0,0 +1,247 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { locationService, setDataSourceSrv } from '@grafana/runtime'; +import { dateTime } from '@grafana/data'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import { fetchSilences, fetchAlerts, createOrUpdateSilence } from './api/alertmanager'; +import { typeAsJestMock } from 'test/helpers/typeAsJestMock'; +import { configureStore } from 'app/store/configureStore'; +import Silences from './Silences'; +import { mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks'; +import { DataSourceType } from './utils/datasource'; +import { parseMatchers } from './utils/alertmanager'; +import { AlertState, MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; +import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; +import userEvent from '@testing-library/user-event'; + +jest.mock('./api/alertmanager'); + +const mocks = { + api: { + fetchSilences: typeAsJestMock(fetchSilences), + fetchAlerts: typeAsJestMock(fetchAlerts), + createOrUpdateSilence: typeAsJestMock(createOrUpdateSilence), + }, +}; + +const renderSilences = (location = '/alerting/silences/') => { + const store = configureStore(); + locationService.push(location); + + return render( + + + + + + ); +}; + +const dataSources = { + am: mockDataSource({ + name: 'Alertmanager', + type: DataSourceType.Alertmanager, + }), +}; + +const ui = { + silencesTable: byTestId('silences-table'), + silenceRow: byTestId('silence-table-row'), + silencedAlertCell: byTestId('silenced-alerts'), + queryBar: byPlaceholderText('Search'), + editor: { + timeRange: byLabelText('Timepicker', { exact: false }), + durationField: byLabelText('Duration'), + durationInput: byRole('textbox', { name: /duration/i }), + matchersField: byTestId('matcher'), + matcherName: byPlaceholderText('label'), + matcherValue: byPlaceholderText('value'), + comment: byPlaceholderText('Details about the silence'), + createdBy: byPlaceholderText('Username'), + matcherOperatorSelect: byLabelText('operator'), + matcherOperator: (operator: MatcherOperator) => byText(operator, { exact: true }), + addMatcherButton: byRole('button', { name: 'Add matcher' }), + submit: byText('Submit'), + }, +}; + +const resetMocks = () => { + jest.resetAllMocks(); + mocks.api.fetchSilences.mockImplementation(() => { + return Promise.resolve([ + mockSilence({ id: '12345' }), + mockSilence({ id: '67890', matchers: parseMatchers('foo!=bar'), comment: 'Catch all' }), + ]); + }); + + mocks.api.fetchAlerts.mockImplementation(() => { + return Promise.resolve([ + mockAlertmanagerAlert({ + labels: { foo: 'bar' }, + status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, + }), + mockAlertmanagerAlert({ + labels: { foo: 'buzz' }, + status: { state: AlertState.Suppressed, silencedBy: ['67890'], inhibitedBy: [] }, + }), + ]); + }); + + mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence()); +}; + +describe('Silences', () => { + beforeAll(resetMocks); + afterEach(resetMocks); + + beforeEach(() => { + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + }); + + it('loads and shows silences', async () => { + renderSilences(); + await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); + await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); + + expect(ui.silencesTable.query()).not.toBeNull(); + + const silences = ui.silenceRow.queryAll(); + expect(silences).toHaveLength(2); + expect(silences[0]).toHaveTextContent('foo=bar'); + expect(silences[1]).toHaveTextContent('foo!=bar'); + }); + + it('shows the correct number of silenced alerts', async () => { + mocks.api.fetchAlerts.mockImplementation(() => { + return Promise.resolve([ + mockAlertmanagerAlert({ + labels: { foo: 'bar', buzz: 'bazz' }, + status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, + }), + mockAlertmanagerAlert({ + labels: { foo: 'bar', buzz: 'bazz' }, + status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, + }), + ]); + }); + + renderSilences(); + await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); + await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); + + const silencedAlertRows = ui.silencedAlertCell.getAll(ui.silencesTable.get()); + expect(silencedAlertRows).toHaveLength(2); + expect(silencedAlertRows[0]).toHaveTextContent('2'); + expect(silencedAlertRows[1]).toHaveTextContent('0'); + }); + + it('filters silences by matchers', async () => { + renderSilences(); + await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); + await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); + + const queryBar = ui.queryBar.get(); + userEvent.paste(queryBar, 'foo=bar'); + + await waitFor(() => expect(ui.silenceRow.getAll()).toHaveLength(1)); + }); +}); + +describe('Silence edit', () => { + const baseUrlPath = '/alerting/silence/new'; + beforeAll(resetMocks); + afterEach(resetMocks); + + beforeEach(() => { + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + }); + + it('prefills the matchers field with matchers params', async () => { + renderSilences( + `${baseUrlPath}?matchers=${encodeURIComponent('foo=bar,bar=~ba.+,hello!=world,cluster!~us-central.*')}` + ); + await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull()); + + const matchers = ui.editor.matchersField.queryAll(); + expect(matchers).toHaveLength(4); + + expect(ui.editor.matcherName.query(matchers[0])).toHaveValue('foo'); + expect(ui.editor.matcherOperator(MatcherOperator.equal).query(matchers[0])).not.toBeNull(); + expect(ui.editor.matcherValue.query(matchers[0])).toHaveValue('bar'); + + expect(ui.editor.matcherName.query(matchers[1])).toHaveValue('bar'); + expect(ui.editor.matcherOperator(MatcherOperator.regex).query(matchers[1])).not.toBeNull(); + expect(ui.editor.matcherValue.query(matchers[1])).toHaveValue('ba.+'); + + expect(ui.editor.matcherName.query(matchers[2])).toHaveValue('hello'); + expect(ui.editor.matcherOperator(MatcherOperator.notEqual).query(matchers[2])).not.toBeNull(); + expect(ui.editor.matcherValue.query(matchers[2])).toHaveValue('world'); + + expect(ui.editor.matcherName.query(matchers[3])).toHaveValue('cluster'); + expect(ui.editor.matcherOperator(MatcherOperator.notRegex).query(matchers[3])).not.toBeNull(); + expect(ui.editor.matcherValue.query(matchers[3])).toHaveValue('us-central.*'); + }); + + it('creates a new silence', async () => { + renderSilences(baseUrlPath); + await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull()); + + const start = new Date(); + const end = new Date(start.getTime() + 24 * 60 * 60 * 1000); + + const startDateString = dateTime(start).format('YYYY-MM-DD'); + const endDateString = dateTime(end).format('YYYY-MM-DD'); + + userEvent.clear(ui.editor.durationInput.get()); + await userEvent.type(ui.editor.durationInput.get(), '1d'); + + await waitFor(() => expect(ui.editor.durationInput.query()).toHaveValue('1d')); + await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(startDateString)); + await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(endDateString)); + + await userEvent.type(ui.editor.matcherName.get(), 'foo'); + await userEvent.type(ui.editor.matcherOperatorSelect.get(), '='); + userEvent.tab(); + await userEvent.type(ui.editor.matcherValue.get(), 'bar'); + + userEvent.click(ui.editor.addMatcherButton.get()); + await userEvent.type(ui.editor.matcherName.getAll()[1], 'bar'); + await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[1], '!='); + userEvent.tab(); + await userEvent.type(ui.editor.matcherValue.getAll()[1], 'buzz'); + + userEvent.click(ui.editor.addMatcherButton.get()); + await userEvent.type(ui.editor.matcherName.getAll()[2], 'region'); + await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[2], '=~'); + userEvent.tab(); + await userEvent.type(ui.editor.matcherValue.getAll()[2], 'us-west-.*'); + + userEvent.click(ui.editor.addMatcherButton.get()); + await userEvent.type(ui.editor.matcherName.getAll()[3], 'env'); + await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[3], '!~'); + userEvent.tab(); + await userEvent.type(ui.editor.matcherValue.getAll()[3], 'dev|staging'); + + await userEvent.type(ui.editor.comment.get(), 'Test'); + await userEvent.type(ui.editor.createdBy.get(), 'Homer Simpson'); + + userEvent.click(ui.editor.submit.get()); + + await waitFor(() => + expect(mocks.api.createOrUpdateSilence).toHaveBeenCalledWith( + 'grafana', + expect.objectContaining({ + comment: 'Test', + createdBy: 'Homer Simpson', + matchers: [ + { isEqual: true, isRegex: false, name: 'foo', value: 'bar' }, + { isEqual: false, isRegex: false, name: 'bar', value: 'buzz' }, + { isEqual: true, isRegex: true, name: 'region', value: 'us-west-.*' }, + { isEqual: false, isRegex: true, name: 'env', value: 'dev|staging' }, + ], + }) + ) + ); + }); +}); diff --git a/public/app/features/alerting/unified/components/silences/MatchersField.tsx b/public/app/features/alerting/unified/components/silences/MatchersField.tsx index ae3774500ab..96ee773c510 100644 --- a/public/app/features/alerting/unified/components/silences/MatchersField.tsx +++ b/public/app/features/alerting/unified/components/silences/MatchersField.tsx @@ -29,7 +29,7 @@ const MatchersField: FC = ({ className }) => {
{matchers.map((matcher, index) => { return ( -
+
= ({ className }) => { render={({ field: { onChange, ref, ...field } }) => (