mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
* feat(Plugins/Catalog): start adding necessary apis * feat(PLugins/Catalog): add extra helpers for merging local & remote plugins * feat(Plugins/Catalog): add plugin details as an optional field of CatalogPlugin * feat(PLugins/Catalog): add scaffolding for the new redux model * feat(PLugins/Catalog): export reducers based on a feature-flag * refactor(Plugins/Admin): rename api methods * feat(Plugin/Catalog): add an api method for fetching a single plugin * feat(Plugins/Admin): try cleaning stuff around plugin fetching * ffeat(Plugins/Catalog): return the catalog reducer when the feature flag is set * refactor(Plugins/Admin): fix typings * feat(Plugins/Admin): use the new reducer for the browse page * feat(catalog): introduce selectors to search and filter plugins list * refactor(Plugins/Details): rename page prop type * refactor(Plugins/Admin): add a const for a state prefix * refactor(Plugins/Admin): use the state prefix in the actions * feat(Plugins/Admin): add types for the requests * refactor(Plugins/Admin): add request info to the reducer * refactor(Plugins/Admin): add request handling to the hooks & selectors * refactor(Plugins/Details): start using the data stored in Redux * refactor(Plugins/Admin): rename selector to start with "select" * fix(Plugins/Admin): only fetch plugins once * refactor(Plugins/Admin): make the tab selection work in details * refactor(catalog): put back loading and error states in plugin list * refactor(Plugins/Admin): use CatalogPlugin for <PluginDetailsSignature /> * feat(Plugins/Admin): add an api method for fetching plugin details * refactor(Plugins/Admin): add action for updating the details * irefactor(Plugins/Admin): show basic plugin details info * refactor(Plugin Details): migrate the plugin details header * refactor(Plugins/Admin): make the config and dashboards tabs work * refactor(Plugins/Admin): add old reducer state to the new one * feat(catalog): introduce actions, reducers and hooks for install & uninstall * refactor(catalog): wire up InstallControls component to redux * refactor(catalog): move parentUrl inside PluginDetailsHeader and uncomment InstallControls * feat(catalog): introduce code for plugin updates to install action * refactor(Plugins/Admin): add backward compatible actions * test(catalog): update PluginDetails and Browse tests to work with catalog store * refactor(Plugins/Admin): make the dashboards and panels work again * refactor(Plugins/Admin): fix linter and typescript errors * fix(Plugins/Admin): put the local-only plugins to the beginning of the list * fix(Plugins/Admin): fix the mocks in the tests for PluginDetails * refactor(Plugins/Admin): remove unecessary hook usePluginsByFilter() * refactor(Plugins/Admin): extract the useTabs() hook to its own file * refactor(Plugins/Admin): remove unused helpers and types * fix(Plugins/Admin): show the first tab when uninstalling an app plugin This can cause the user to find themselves on a dissappeared tab, as the config and dashboards tabs are removed. * fix(catalog): correct logic for checking if activeTabIndex is greater than total tabs * fix(Plugins/Admin): fix race-condition between fetching plugin details and all plugins * fix(Plugins): fix strict type errors * chore(catalog): remove todos * feat(catalog): render an alert in PluginDetails when a plugin cannot be found * feat(catalog): use the proper store state * refactor(Plugins/Admin): fetch local and remote plugins in parallell Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * style(catalog): fix prettier error in api * fix(catalog): prevent throwing error if InstallControlsButton is unmounted during install * refactor(Plugins/Admin): add a separate hook for filtering & sorting plugins Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
201 lines
5.8 KiB
TypeScript
201 lines
5.8 KiB
TypeScript
import { config } from '@grafana/runtime';
|
|
import { gt } from 'semver';
|
|
import { PluginSignatureStatus, dateTimeParse } from '@grafana/data';
|
|
import { CatalogPlugin, LocalPlugin, RemotePlugin } from './types';
|
|
import { contextSrv } from 'app/core/services/context_srv';
|
|
|
|
export function isGrafanaAdmin(): boolean {
|
|
return config.bootData.user.isGrafanaAdmin;
|
|
}
|
|
|
|
export function isOrgAdmin() {
|
|
return contextSrv.hasRole('Admin');
|
|
}
|
|
|
|
export function mergeLocalsAndRemotes(local: LocalPlugin[] = [], remote: RemotePlugin[] = []): CatalogPlugin[] {
|
|
const catalogPlugins: CatalogPlugin[] = [];
|
|
|
|
// add locals
|
|
local.forEach((l) => {
|
|
const remotePlugin = remote.find((r) => r.slug === l.id);
|
|
|
|
if (!remotePlugin) {
|
|
catalogPlugins.push(mergeLocalAndRemote(l));
|
|
}
|
|
});
|
|
|
|
// add remote
|
|
remote.forEach((r) => {
|
|
const localPlugin = local.find((l) => l.id === r.slug);
|
|
|
|
catalogPlugins.push(mergeLocalAndRemote(localPlugin, r));
|
|
});
|
|
|
|
return catalogPlugins;
|
|
}
|
|
|
|
export function mergeLocalAndRemote(local?: LocalPlugin, remote?: RemotePlugin): CatalogPlugin {
|
|
if (!local && remote) {
|
|
return mapRemoteToCatalog(remote);
|
|
}
|
|
|
|
if (local && !remote) {
|
|
return mapLocalToCatalog(local);
|
|
}
|
|
|
|
return mapToCatalogPlugin(local, remote);
|
|
}
|
|
|
|
export function mapRemoteToCatalog(plugin: RemotePlugin): CatalogPlugin {
|
|
const {
|
|
name,
|
|
slug: id,
|
|
description,
|
|
version,
|
|
orgName,
|
|
popularity,
|
|
downloads,
|
|
typeCode,
|
|
updatedAt,
|
|
createdAt: publishedAt,
|
|
status,
|
|
versionSignatureType,
|
|
signatureType,
|
|
} = plugin;
|
|
|
|
const hasSignature = signatureType !== '' || versionSignatureType !== '';
|
|
const catalogPlugin = {
|
|
description,
|
|
downloads,
|
|
id,
|
|
info: {
|
|
logos: {
|
|
small: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/small`,
|
|
large: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/large`,
|
|
},
|
|
},
|
|
name,
|
|
orgName,
|
|
popularity,
|
|
publishedAt,
|
|
signature: hasSignature ? PluginSignatureStatus.valid : PluginSignatureStatus.missing,
|
|
updatedAt,
|
|
version,
|
|
hasUpdate: false,
|
|
isInstalled: false,
|
|
isCore: plugin.internal,
|
|
isDev: false,
|
|
isEnterprise: status === 'enterprise',
|
|
type: typeCode,
|
|
};
|
|
return catalogPlugin;
|
|
}
|
|
|
|
export function mapLocalToCatalog(plugin: LocalPlugin): CatalogPlugin {
|
|
const {
|
|
name,
|
|
info: { description, version, logos, updated, author },
|
|
id,
|
|
signature,
|
|
dev,
|
|
type,
|
|
signatureOrg,
|
|
signatureType,
|
|
} = plugin;
|
|
|
|
return {
|
|
description,
|
|
downloads: 0,
|
|
id,
|
|
info: { logos },
|
|
name,
|
|
orgName: author.name,
|
|
popularity: 0,
|
|
publishedAt: '',
|
|
signature,
|
|
signatureOrg,
|
|
signatureType,
|
|
updatedAt: updated,
|
|
version,
|
|
hasUpdate: false,
|
|
isInstalled: true,
|
|
isCore: signature === 'internal',
|
|
isDev: Boolean(dev),
|
|
isEnterprise: false,
|
|
type,
|
|
};
|
|
}
|
|
|
|
export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin): CatalogPlugin {
|
|
const version = remote?.version || local?.info.version || '';
|
|
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 !== '';
|
|
let logos = {
|
|
small: 'https://grafana.com/api/plugins/404notfound/versions/none/logos/small',
|
|
large: 'https://grafana.com/api/plugins/404notfound/versions/none/logos/large',
|
|
};
|
|
|
|
if (remote) {
|
|
logos = {
|
|
small: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/small`,
|
|
large: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/large`,
|
|
};
|
|
} else if (local && local.info.logos) {
|
|
logos = local.info.logos;
|
|
}
|
|
|
|
return {
|
|
description: remote?.description || local?.info.description || '',
|
|
downloads: remote?.downloads || 0,
|
|
hasUpdate,
|
|
id,
|
|
info: {
|
|
logos,
|
|
},
|
|
isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal),
|
|
isDev: Boolean(local?.dev),
|
|
isEnterprise: remote?.status === 'enterprise',
|
|
isInstalled: Boolean(local),
|
|
name: remote?.name || local?.name || '',
|
|
orgName: remote?.orgName || local?.info.author.name || '',
|
|
popularity: remote?.popularity || 0,
|
|
publishedAt: remote?.createdAt || '',
|
|
type: remote?.typeCode || local?.type,
|
|
signature: local?.signature || (hasRemoteSignature ? PluginSignatureStatus.valid : PluginSignatureStatus.missing),
|
|
signatureOrg: local?.signatureOrg || remote?.versionSignedByOrgName,
|
|
signatureType: local?.signatureType || remote?.versionSignatureType || remote?.signatureType || undefined,
|
|
updatedAt: remote?.updatedAt || local?.info.updated || '',
|
|
version,
|
|
};
|
|
}
|
|
|
|
export const getExternalManageLink = (pluginId: string) => `https://grafana.com/grafana/plugins/${pluginId}`;
|
|
|
|
export enum Sorters {
|
|
nameAsc = 'nameAsc',
|
|
nameDesc = 'nameDesc',
|
|
updated = 'updated',
|
|
published = 'published',
|
|
downloads = 'downloads',
|
|
}
|
|
|
|
export const sortPlugins = (plugins: CatalogPlugin[], sortBy: Sorters) => {
|
|
const sorters: { [name: string]: (a: CatalogPlugin, b: CatalogPlugin) => number } = {
|
|
nameAsc: (a: CatalogPlugin, b: CatalogPlugin) => a.name.localeCompare(b.name),
|
|
nameDesc: (a: CatalogPlugin, b: CatalogPlugin) => b.name.localeCompare(a.name),
|
|
updated: (a: CatalogPlugin, b: CatalogPlugin) =>
|
|
dateTimeParse(b.updatedAt).valueOf() - dateTimeParse(a.updatedAt).valueOf(),
|
|
published: (a: CatalogPlugin, b: CatalogPlugin) =>
|
|
dateTimeParse(b.publishedAt).valueOf() - dateTimeParse(a.publishedAt).valueOf(),
|
|
downloads: (a: CatalogPlugin, b: CatalogPlugin) => b.downloads - a.downloads,
|
|
};
|
|
|
|
if (sorters[sortBy]) {
|
|
return plugins.sort(sorters[sortBy]);
|
|
}
|
|
|
|
return plugins;
|
|
};
|