Compare commits
2 Commits
wb/pkg-plu
...
ash/react-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f164d9f2 | ||
|
|
86fc051c58 |
@@ -22,7 +22,7 @@ import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './c
|
|||||||
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
type RenderOptions = {
|
type RenderOptions = {
|
||||||
canvasRef: RefObject<HTMLCanvasElement>;
|
canvasRef: RefObject<HTMLCanvasElement | null>;
|
||||||
data: FlameGraphDataContainer;
|
data: FlameGraphDataContainer;
|
||||||
root: LevelItem;
|
root: LevelItem;
|
||||||
direction: 'children' | 'parents';
|
direction: 'children' | 'parents';
|
||||||
@@ -373,7 +373,7 @@ function useColorFunction(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement>, wrapperWidth: number, numberOfLevels: number) {
|
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement | null>, wrapperWidth: number, numberOfLevels: number) {
|
||||||
const [ctx, setCtx] = useState<CanvasRenderingContext2D>();
|
const [ctx, setCtx] = useState<CanvasRenderingContext2D>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const CAUGHT_KEYS = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', 'Tab'];
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export interface UseListFocusProps {
|
export interface UseListFocusProps {
|
||||||
localRef: RefObject<HTMLUListElement>;
|
localRef: RefObject<HTMLUListElement | null>;
|
||||||
options: TimeOption[];
|
options: TimeOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { RefObject, useRef } from 'react';
|
import { RefObject, useRef } from 'react';
|
||||||
|
|
||||||
export function useFocus(): [RefObject<HTMLInputElement>, () => void] {
|
export function useFocus(): [RefObject<HTMLInputElement | null>, () => void] {
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const setFocus = () => {
|
const setFocus = () => {
|
||||||
ref.current && ref.current.focus();
|
ref.current && ref.current.focus();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const UNFOCUSED = -1;
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export interface UseMenuFocusProps {
|
export interface UseMenuFocusProps {
|
||||||
localRef: RefObject<HTMLDivElement>;
|
localRef: RefObject<HTMLDivElement | null>;
|
||||||
isMenuOpen?: boolean;
|
isMenuOpen?: boolean;
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
onOpen?: (focusOnItem: (itemId: number) => void) => void;
|
onOpen?: (focusOnItem: (itemId: number) => void) => void;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface Props extends Omit<BoxProps, 'display' | 'direction' | 'element' | 'fl
|
|||||||
*
|
*
|
||||||
* https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-scrollcontainer--docs
|
* https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-scrollcontainer--docs
|
||||||
*/
|
*/
|
||||||
export const ScrollContainer = forwardRef<HTMLDivElement, PropsWithChildren<Props>>(
|
export const ScrollContainer = forwardRef<HTMLDivElement | null, PropsWithChildren<Props>>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface TableCellTooltipProps {
|
|||||||
field: Field;
|
field: Field;
|
||||||
getActions: (field: Field, rowIdx: number) => ActionModel[];
|
getActions: (field: Field, rowIdx: number) => ActionModel[];
|
||||||
getTextColorForBackground: (bgColor: string) => string;
|
getTextColorForBackground: (bgColor: string) => string;
|
||||||
gridRef: RefObject<DataGridHandle>;
|
gridRef: RefObject<DataGridHandle | null>;
|
||||||
height: number;
|
height: number;
|
||||||
placement?: TableCellTooltipPlacement;
|
placement?: TableCellTooltipPlacement;
|
||||||
renderer: TableCellRenderer;
|
renderer: TableCellRenderer;
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ export function useColumnResize(
|
|||||||
return dataGridResizeHandler;
|
return dataGridResizeHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScrollbarWidth(ref: RefObject<DataGridHandle>, height: number) {
|
export function useScrollbarWidth(ref: RefObject<DataGridHandle | null>, height: number) {
|
||||||
const [scrollbarWidth, setScrollbarWidth] = useState(0);
|
const [scrollbarWidth, setScrollbarWidth] = useState(0);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export const Table = memo((props: Props) => {
|
|||||||
// `useTableStateReducer`, which is needed to construct options for `useTable` (the hook that returns
|
// `useTableStateReducer`, which is needed to construct options for `useTable` (the hook that returns
|
||||||
// `toggleAllRowsExpanded`), and if we used a variable, that variable would be undefined at the time
|
// `toggleAllRowsExpanded`), and if we used a variable, that variable would be undefined at the time
|
||||||
// we initialize `useTableStateReducer`.
|
// we initialize `useTableStateReducer`.
|
||||||
const toggleAllRowsExpandedRef = useRef<(value?: boolean) => void>();
|
const toggleAllRowsExpandedRef = useRef<((value?: boolean) => void) | null>(null);
|
||||||
|
|
||||||
// Internal react table state reducer
|
// Internal react table state reducer
|
||||||
const stateReducer = useTableStateReducer({
|
const stateReducer = useTableStateReducer({
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { GrafanaTableState } from './types';
|
|||||||
Select the scrollbar element from the VariableSizeList scope
|
Select the scrollbar element from the VariableSizeList scope
|
||||||
*/
|
*/
|
||||||
export function useFixScrollbarContainer(
|
export function useFixScrollbarContainer(
|
||||||
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement>,
|
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement | null>,
|
||||||
tableDivRef: React.RefObject<HTMLDivElement>
|
tableDivRef: React.RefObject<HTMLDivElement | null>
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (variableSizeListScrollbarRef.current && tableDivRef.current) {
|
if (variableSizeListScrollbarRef.current && tableDivRef.current) {
|
||||||
@@ -43,7 +43,7 @@ export function useFixScrollbarContainer(
|
|||||||
*/
|
*/
|
||||||
export function useResetVariableListSizeCache(
|
export function useResetVariableListSizeCache(
|
||||||
extendedState: GrafanaTableState,
|
extendedState: GrafanaTableState,
|
||||||
listRef: React.RefObject<VariableSizeList>,
|
listRef: React.RefObject<VariableSizeList | null>,
|
||||||
data: DataFrame,
|
data: DataFrame,
|
||||||
hasUniqueId: boolean
|
hasUniqueId: boolean
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface EventsCanvasProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) {
|
export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) {
|
||||||
const plotInstance = useRef<uPlot>();
|
const plotInstance = useRef<uPlot | null>(null);
|
||||||
// render token required to re-render annotation markers. Rendering lines happens in uPlot and the props do not change
|
// render token required to re-render annotation markers. Rendering lines happens in uPlot and the props do not change
|
||||||
// so we need to force the re-render when the draw hook was performed by uPlot
|
// so we need to force the re-render when the draw hook was performed by uPlot
|
||||||
const [renderToken, setRenderToken] = useState(0);
|
const [renderToken, setRenderToken] = useState(0);
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export const TooltipPlugin2 = ({
|
|||||||
|
|
||||||
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState);
|
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState);
|
||||||
|
|
||||||
const sizeRef = useRef<TooltipContainerSize>();
|
const sizeRef = useRef<TooltipContainerSize | null>(null);
|
||||||
const styles = useStyles2(getStyles, maxWidth);
|
const styles = useStyles2(getStyles, maxWidth);
|
||||||
|
|
||||||
const renderRef = useRef(render);
|
const renderRef = useRef(render);
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export interface GraphNGState {
|
|||||||
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
||||||
static contextType = PanelContextRoot;
|
static contextType = PanelContextRoot;
|
||||||
panelContext: PanelContext = {} as PanelContext;
|
panelContext: PanelContext = {} as PanelContext;
|
||||||
private plotInstance: React.RefObject<uPlot>;
|
private plotInstance: React.RefObject<uPlot | null>;
|
||||||
|
|
||||||
private subscription = new Subscription();
|
private subscription = new Subscription();
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginassets/modulehash"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
@@ -82,8 +80,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
|||||||
var pluginsAssets = passets
|
var pluginsAssets = passets
|
||||||
if pluginsAssets == nil {
|
if pluginsAssets == nil {
|
||||||
sig := signature.ProvideService(pluginsCfg, statickey.New())
|
sig := signature.ProvideService(pluginsCfg, statickey.New())
|
||||||
calc := modulehash.NewCalculator(pluginsCfg, registry.NewInMemory(), pluginsCDN, sig)
|
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore)
|
||||||
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, calc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
@@ -717,8 +714,6 @@ func newPluginAssets() func() *pluginassets.Service {
|
|||||||
|
|
||||||
func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service {
|
func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service {
|
||||||
return func() *pluginassets.Service {
|
return func() *pluginassets.Service {
|
||||||
cdn := pluginscdn.ProvideService(pCfg)
|
return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), signature.ProvideService(pCfg, statickey.New()), &pluginstore.FakePluginStore{})
|
||||||
calc := modulehash.NewCalculator(pCfg, registry.NewInMemory(), cdn, signature.ProvideService(pCfg, statickey.New()))
|
|
||||||
return pluginassets.ProvideService(pCfg, cdn, calc)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginassets/modulehash"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
|
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
@@ -850,8 +849,7 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
pCfg := &config.PluginManagementCfg{}
|
pCfg := &config.PluginManagementCfg{}
|
||||||
pluginCDN := pluginscdn.ProvideService(pCfg)
|
pluginCDN := pluginscdn.ProvideService(pCfg)
|
||||||
sig := signature.ProvideService(pCfg, statickey.New())
|
sig := signature.ProvideService(pCfg, statickey.New())
|
||||||
calc := modulehash.NewCalculator(pCfg, registry.NewInMemory(), pluginCDN, sig)
|
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore)
|
||||||
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, calc)
|
|
||||||
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
||||||
hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService(
|
hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService(
|
||||||
hs.Cfg,
|
hs.Cfg,
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
package modulehash
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/log"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Calculator struct {
|
|
||||||
reg registry.Service
|
|
||||||
cfg *config.PluginManagementCfg
|
|
||||||
cdn *pluginscdn.Service
|
|
||||||
signature *signature.Signature
|
|
||||||
log log.Logger
|
|
||||||
|
|
||||||
moduleHashCache sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCalculator(cfg *config.PluginManagementCfg, reg registry.Service, cdn *pluginscdn.Service, signature *signature.Signature) *Calculator {
|
|
||||||
return &Calculator{
|
|
||||||
cfg: cfg,
|
|
||||||
reg: reg,
|
|
||||||
cdn: cdn,
|
|
||||||
signature: signature,
|
|
||||||
log: log.New("modulehash"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
|
|
||||||
// The module hash is read from the plugin's MANIFEST.txt file.
|
|
||||||
// The plugin can also be a nested plugin.
|
|
||||||
// If the plugin is unsigned, an empty string is returned.
|
|
||||||
// The results are cached to avoid repeated reads from the MANIFEST.txt file.
|
|
||||||
func (c *Calculator) ModuleHash(ctx context.Context, pluginID, pluginVersion string) string {
|
|
||||||
p, ok := c.reg.Plugin(ctx, pluginID, pluginVersion)
|
|
||||||
if !ok {
|
|
||||||
c.log.Error("Failed to calculate module hash as plugin is not registered", "pluginId", pluginID)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
k := c.moduleHashCacheKey(pluginID, pluginVersion)
|
|
||||||
cachedValue, ok := c.moduleHashCache.Load(k)
|
|
||||||
if ok {
|
|
||||||
return cachedValue.(string)
|
|
||||||
}
|
|
||||||
mh, err := c.moduleHash(ctx, p, "")
|
|
||||||
if err != nil {
|
|
||||||
c.log.Error("Failed to calculate module hash", "pluginId", p.ID, "error", err)
|
|
||||||
}
|
|
||||||
c.moduleHashCache.Store(k, mh)
|
|
||||||
return mh
|
|
||||||
}
|
|
||||||
|
|
||||||
// moduleHash is the underlying function for ModuleHash. See its documentation for more information.
|
|
||||||
// If the plugin is not a CDN plugin, the function will return an empty string.
|
|
||||||
// It will read the module hash from the MANIFEST.txt in the [[plugins.FS]] of the provided plugin.
|
|
||||||
// If childFSBase is provided, the function will try to get the hash from MANIFEST.txt for the provided children's
|
|
||||||
// module.js file, rather than for the provided plugin.
|
|
||||||
func (c *Calculator) moduleHash(ctx context.Context, p *plugins.Plugin, childFSBase string) (r string, err error) {
|
|
||||||
if !c.cfg.Features.SriChecksEnabled {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore unsigned plugins
|
|
||||||
if !p.Signature.IsValid() {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Parent != nil {
|
|
||||||
// The module hash is contained within the parent's MANIFEST.txt file.
|
|
||||||
// For example, the parent's MANIFEST.txt will contain an entry similar to this:
|
|
||||||
//
|
|
||||||
// ```
|
|
||||||
// "datasource/module.js": "1234567890abcdef..."
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// Recursively call moduleHash with the parent plugin and with the children plugin folder path
|
|
||||||
// to get the correct module hash for the nested plugin.
|
|
||||||
if childFSBase == "" {
|
|
||||||
childFSBase = p.FS.Base()
|
|
||||||
}
|
|
||||||
return c.moduleHash(ctx, p.Parent, childFSBase)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only CDN plugins are supported for SRI checks.
|
|
||||||
// CDN plugins have the version as part of the URL, which acts as a cache-buster.
|
|
||||||
// Needed due to: https://github.com/grafana/plugin-tools/pull/1426
|
|
||||||
// FS plugins build before this change will have SRI mismatch issues.
|
|
||||||
if !c.cdnEnabled(p.ID, p.FS) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest, err := c.signature.ReadPluginManifestFromFS(ctx, p.FS)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read plugin manifest: %w", err)
|
|
||||||
}
|
|
||||||
if !manifest.IsV2() {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var childPath string
|
|
||||||
if childFSBase != "" {
|
|
||||||
// Calculate the relative path of the child plugin folder from the parent plugin folder.
|
|
||||||
childPath, err = p.FS.Rel(childFSBase)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("rel path: %w", err)
|
|
||||||
}
|
|
||||||
// MANIFETS.txt uses forward slashes as path separators.
|
|
||||||
childPath = filepath.ToSlash(childPath)
|
|
||||||
}
|
|
||||||
moduleHash, ok := manifest.Files[path.Join(childPath, "module.js")]
|
|
||||||
if !ok {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return convertHashForSRI(moduleHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Calculator) cdnEnabled(pluginID string, fs plugins.FS) bool {
|
|
||||||
return c.cdn.PluginSupported(pluginID) || fs.Type().CDN()
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks.
|
|
||||||
func convertHashForSRI(h string) (string, error) {
|
|
||||||
hb, err := hex.DecodeString(h)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("hex decode string: %w", err)
|
|
||||||
}
|
|
||||||
return "sha256-" + base64.StdEncoding.EncodeToString(hb), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// moduleHashCacheKey returns a unique key for the module hash cache.
|
|
||||||
func (c *Calculator) moduleHashCacheKey(pluginId, pluginVersion string) string {
|
|
||||||
return pluginId + ":" + pluginVersion
|
|
||||||
}
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
package modulehash
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ModuleHash(t *testing.T) {
|
|
||||||
const (
|
|
||||||
pluginID = "grafana-test-datasource"
|
|
||||||
parentPluginID = "grafana-test-app"
|
|
||||||
)
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
features *config.Features
|
|
||||||
registry []*plugins.Plugin
|
|
||||||
|
|
||||||
// Can be used to configure plugin's fs
|
|
||||||
// fs cdn type = loaded from CDN with no files on disk
|
|
||||||
// fs local type = files on disk but served from CDN only if cdn=true
|
|
||||||
plugin string
|
|
||||||
|
|
||||||
// When true, set cdn=true in config
|
|
||||||
cdn bool
|
|
||||||
expModuleHash string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "unsigned should not return module hash",
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(pluginID, withSignatureStatus(plugins.SignatureStatusUnsigned))},
|
|
||||||
cdn: false,
|
|
||||||
features: &config.Features{SriChecksEnabled: false},
|
|
||||||
expModuleHash: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
|
||||||
withClass(plugins.ClassExternal),
|
|
||||||
)},
|
|
||||||
cdn: true,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
|
||||||
withClass(plugins.ClassExternal),
|
|
||||||
)},
|
|
||||||
cdn: true,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
|
||||||
)},
|
|
||||||
cdn: false,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
|
||||||
)},
|
|
||||||
cdn: true,
|
|
||||||
features: &config.Features{SriChecksEnabled: false},
|
|
||||||
expModuleHash: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
|
||||||
)},
|
|
||||||
cdn: false,
|
|
||||||
features: &config.Features{SriChecksEnabled: false},
|
|
||||||
expModuleHash: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// parentPluginID (/)
|
|
||||||
// └── pluginID (/datasource)
|
|
||||||
name: "nested plugin should return module hash from parent MANIFEST.txt",
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{
|
|
||||||
newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested", "datasource"))),
|
|
||||||
withParent(newPlugin(
|
|
||||||
parentPluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested"))),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
cdn: true,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: newSRIHash(t, "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// parentPluginID (/)
|
|
||||||
// └── pluginID (/panels/one)
|
|
||||||
name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt",
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{
|
|
||||||
newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested", "panels", "one"))),
|
|
||||||
withParent(newPlugin(
|
|
||||||
parentPluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested"))),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
cdn: true,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// grand-parent-app (/)
|
|
||||||
// ├── parent-datasource (/datasource)
|
|
||||||
// │ └── child-panel (/datasource/panels/one)
|
|
||||||
name: "nested plugin of a nested plugin should return module hash from parent MANIFEST.txt",
|
|
||||||
registry: []*plugins.Plugin{
|
|
||||||
newPlugin(
|
|
||||||
"child-panel",
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))),
|
|
||||||
withParent(newPlugin(
|
|
||||||
"parent-datasource",
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-deeply-nested", "datasource"))),
|
|
||||||
withParent(newPlugin(
|
|
||||||
"grand-parent-app",
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-deeply-nested"))),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
plugin: "child-panel",
|
|
||||||
cdn: true,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nested plugin should not return module hash from parent if it's not registered in the registry",
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested", "panels", "one"))),
|
|
||||||
withParent(newPlugin(
|
|
||||||
parentPluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested"))),
|
|
||||||
)),
|
|
||||||
)},
|
|
||||||
cdn: false,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing module.js entry from MANIFEST.txt should not return module hash",
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-no-module-js"))),
|
|
||||||
)},
|
|
||||||
cdn: false,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "signed status but missing MANIFEST.txt should not return module hash",
|
|
||||||
plugin: pluginID,
|
|
||||||
registry: []*plugins.Plugin{newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-no-manifest-txt"))),
|
|
||||||
)},
|
|
||||||
cdn: false,
|
|
||||||
features: &config.Features{SriChecksEnabled: true},
|
|
||||||
expModuleHash: "",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
if tc.name == "" {
|
|
||||||
var expS string
|
|
||||||
if tc.expModuleHash == "" {
|
|
||||||
expS = "should not return module hash"
|
|
||||||
} else {
|
|
||||||
expS = "should return module hash"
|
|
||||||
}
|
|
||||||
tc.name = fmt.Sprintf("feature=%v, cdn_config=%v %s", tc.features.SriChecksEnabled, tc.cdn, expS)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
var pluginSettings config.PluginSettings
|
|
||||||
if tc.cdn {
|
|
||||||
pluginSettings = config.PluginSettings{
|
|
||||||
pluginID: {
|
|
||||||
"cdn": "true",
|
|
||||||
},
|
|
||||||
parentPluginID: map[string]string{
|
|
||||||
"cdn": "true",
|
|
||||||
},
|
|
||||||
"grand-parent-app": map[string]string{
|
|
||||||
"cdn": "true",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
features := tc.features
|
|
||||||
if features == nil {
|
|
||||||
features = &config.Features{}
|
|
||||||
}
|
|
||||||
pCfg := &config.PluginManagementCfg{
|
|
||||||
PluginsCDNURLTemplate: "http://cdn.example.com",
|
|
||||||
PluginSettings: pluginSettings,
|
|
||||||
Features: *features,
|
|
||||||
}
|
|
||||||
|
|
||||||
svc := NewCalculator(
|
|
||||||
pCfg,
|
|
||||||
newPluginRegistry(t, tc.registry...),
|
|
||||||
pluginscdn.ProvideService(pCfg),
|
|
||||||
signature.ProvideService(pCfg, statickey.New()),
|
|
||||||
)
|
|
||||||
mh := svc.ModuleHash(context.Background(), tc.plugin, "")
|
|
||||||
require.Equal(t, tc.expModuleHash, mh)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_ModuleHash_Cache(t *testing.T) {
|
|
||||||
pCfg := &config.PluginManagementCfg{
|
|
||||||
PluginSettings: config.PluginSettings{},
|
|
||||||
Features: config.Features{SriChecksEnabled: true},
|
|
||||||
}
|
|
||||||
svc := NewCalculator(
|
|
||||||
pCfg,
|
|
||||||
newPluginRegistry(t),
|
|
||||||
pluginscdn.ProvideService(pCfg),
|
|
||||||
signature.ProvideService(pCfg, statickey.New()),
|
|
||||||
)
|
|
||||||
const pluginID = "grafana-test-datasource"
|
|
||||||
|
|
||||||
t.Run("cache key", func(t *testing.T) {
|
|
||||||
t.Run("with version", func(t *testing.T) {
|
|
||||||
const pluginVersion = "1.0.0"
|
|
||||||
p := newPlugin(pluginID, withInfo(plugins.Info{Version: pluginVersion}))
|
|
||||||
k := svc.moduleHashCacheKey(p.ID, p.Info.Version)
|
|
||||||
require.Equal(t, pluginID+":"+pluginVersion, k, "cache key should be correct")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("without version", func(t *testing.T) {
|
|
||||||
p := newPlugin(pluginID)
|
|
||||||
k := svc.moduleHashCacheKey(p.ID, p.Info.Version)
|
|
||||||
require.Equal(t, pluginID+":", k, "cache key should be correct")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ModuleHash usage", func(t *testing.T) {
|
|
||||||
pV1 := newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withInfo(plugins.Info{Version: "1.0.0"}),
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
|
||||||
)
|
|
||||||
|
|
||||||
pCfg = &config.PluginManagementCfg{
|
|
||||||
PluginsCDNURLTemplate: "https://cdn.grafana.com",
|
|
||||||
PluginSettings: config.PluginSettings{
|
|
||||||
pluginID: {
|
|
||||||
"cdn": "true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Features: config.Features{SriChecksEnabled: true},
|
|
||||||
}
|
|
||||||
reg := newPluginRegistry(t, pV1)
|
|
||||||
svc = NewCalculator(
|
|
||||||
pCfg,
|
|
||||||
reg,
|
|
||||||
pluginscdn.ProvideService(pCfg),
|
|
||||||
signature.ProvideService(pCfg, statickey.New()),
|
|
||||||
)
|
|
||||||
|
|
||||||
k := svc.moduleHashCacheKey(pV1.ID, pV1.Info.Version)
|
|
||||||
|
|
||||||
_, ok := svc.moduleHashCache.Load(k)
|
|
||||||
require.False(t, ok, "cache should initially be empty")
|
|
||||||
|
|
||||||
mhV1 := svc.ModuleHash(context.Background(), pV1.ID, pV1.Info.Version)
|
|
||||||
pV1Exp := newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03")
|
|
||||||
require.Equal(t, pV1Exp, mhV1, "returned value should be correct")
|
|
||||||
|
|
||||||
cachedMh, ok := svc.moduleHashCache.Load(k)
|
|
||||||
require.True(t, ok)
|
|
||||||
require.Equal(t, pV1Exp, cachedMh, "cache should contain the returned value")
|
|
||||||
|
|
||||||
t.Run("different version uses different cache key", func(t *testing.T) {
|
|
||||||
pV2 := newPlugin(
|
|
||||||
pluginID,
|
|
||||||
withInfo(plugins.Info{Version: "2.0.0"}),
|
|
||||||
withSignatureStatus(plugins.SignatureStatusValid),
|
|
||||||
// different fs for different hash
|
|
||||||
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested"))),
|
|
||||||
)
|
|
||||||
err := reg.Add(context.Background(), pV2)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
mhV2 := svc.ModuleHash(context.Background(), pV2.ID, pV2.Info.Version)
|
|
||||||
require.NotEqual(t, mhV2, mhV1, "different version should have different hash")
|
|
||||||
require.Equal(t, newSRIHash(t, "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a"), mhV2)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cache should be used", func(t *testing.T) {
|
|
||||||
// edit cache directly
|
|
||||||
svc.moduleHashCache.Store(k, "hax")
|
|
||||||
require.Equal(t, "hax", svc.ModuleHash(context.Background(), pV1.ID, pV1.Info.Version))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertHashFromSRI(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
hash string
|
|
||||||
expHash string
|
|
||||||
expErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811",
|
|
||||||
expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hash: "not-a-valid-hash",
|
|
||||||
expErr: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.hash, func(t *testing.T) {
|
|
||||||
r, err := convertHashForSRI(tc.hash)
|
|
||||||
if tc.expErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.expHash, r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPlugin(pluginID string, cbs ...func(p *plugins.Plugin) *plugins.Plugin) *plugins.Plugin {
|
|
||||||
p := &plugins.Plugin{
|
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: pluginID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, cb := range cbs {
|
|
||||||
p = cb(p)
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func withInfo(info plugins.Info) func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
return func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
p.Info = info
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withFS(fs plugins.FS) func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
return func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
p.FS = fs
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withSignatureStatus(status plugins.SignatureStatus) func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
return func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
p.Signature = status
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withParent(parent *plugins.Plugin) func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
return func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
p.Parent = parent
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withClass(class plugins.Class) func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
return func(p *plugins.Plugin) *plugins.Plugin {
|
|
||||||
p.Class = class
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSRIHash(t *testing.T, s string) string {
|
|
||||||
r, err := convertHashForSRI(s)
|
|
||||||
require.NoError(t, err)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginRegistry struct {
|
|
||||||
registry.Service
|
|
||||||
|
|
||||||
reg map[string]*plugins.Plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPluginRegistry(t *testing.T, ps ...*plugins.Plugin) *pluginRegistry {
|
|
||||||
reg := &pluginRegistry{
|
|
||||||
reg: make(map[string]*plugins.Plugin),
|
|
||||||
}
|
|
||||||
for _, p := range ps {
|
|
||||||
err := reg.Add(context.Background(), p)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
return reg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *pluginRegistry) Plugin(_ context.Context, id, version string) (*plugins.Plugin, bool) {
|
|
||||||
key := fmt.Sprintf("%s-%s", id, version)
|
|
||||||
p, exists := f.reg[key]
|
|
||||||
return p, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *pluginRegistry) Add(_ context.Context, p *plugins.Plugin) error {
|
|
||||||
key := fmt.Sprintf("%s-%s", p.ID, p.Info.Version)
|
|
||||||
f.reg[key] = p
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
6
pkg/server/wire_gen.go
generated
6
pkg/server/wire_gen.go
generated
@@ -715,8 +715,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||||
calculator := pluginassets2.ProvideModuleHashCalculator(pluginManagementCfg, pluginscdnService, signatureSignature, inMemory)
|
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
|
||||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, calculator)
|
|
||||||
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
||||||
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
||||||
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
||||||
@@ -1384,8 +1383,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||||
calculator := pluginassets2.ProvideModuleHashCalculator(pluginManagementCfg, pluginscdnService, signatureSignature, inMemory)
|
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
|
||||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, calculator)
|
|
||||||
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
||||||
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
||||||
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ package pluginassets
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginassets/modulehash"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
)
|
)
|
||||||
@@ -24,27 +28,24 @@ var (
|
|||||||
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
|
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service,
|
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, sig *signature.Signature, store pluginstore.Store) *Service {
|
||||||
calc *modulehash.Calculator) *Service {
|
|
||||||
return &Service{
|
return &Service{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
cdn: cdn,
|
cdn: cdn,
|
||||||
|
signature: sig,
|
||||||
|
store: store,
|
||||||
log: log.New("pluginassets"),
|
log: log.New("pluginassets"),
|
||||||
calc: calc,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideModuleHashCalculator(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service,
|
|
||||||
signature *signature.Signature, reg registry.Service) *modulehash.Calculator {
|
|
||||||
return modulehash.NewCalculator(cfg, reg, cdn, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg *config.PluginManagementCfg
|
cfg *config.PluginManagementCfg
|
||||||
cdn *pluginscdn.Service
|
cdn *pluginscdn.Service
|
||||||
calc *modulehash.Calculator
|
signature *signature.Signature
|
||||||
|
store pluginstore.Store
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
|
moduleHashCache sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadingStrategy calculates the loading strategy for a plugin.
|
// LoadingStrategy calculates the loading strategy for a plugin.
|
||||||
@@ -82,8 +83,92 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
|
// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
|
||||||
|
// The module hash is read from the plugin's MANIFEST.txt file.
|
||||||
|
// The plugin can also be a nested plugin.
|
||||||
|
// If the plugin is unsigned, an empty string is returned.
|
||||||
|
// The results are cached to avoid repeated reads from the MANIFEST.txt file.
|
||||||
func (s *Service) ModuleHash(ctx context.Context, p pluginstore.Plugin) string {
|
func (s *Service) ModuleHash(ctx context.Context, p pluginstore.Plugin) string {
|
||||||
return s.calc.ModuleHash(ctx, p.ID, p.Info.Version)
|
k := s.moduleHashCacheKey(p)
|
||||||
|
cachedValue, ok := s.moduleHashCache.Load(k)
|
||||||
|
if ok {
|
||||||
|
return cachedValue.(string)
|
||||||
|
}
|
||||||
|
mh, err := s.moduleHash(ctx, p, "")
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("Failed to calculate module hash", "plugin", p.ID, "error", err)
|
||||||
|
}
|
||||||
|
s.moduleHashCache.Store(k, mh)
|
||||||
|
return mh
|
||||||
|
}
|
||||||
|
|
||||||
|
// moduleHash is the underlying function for ModuleHash. See its documentation for more information.
|
||||||
|
// If the plugin is not a CDN plugin, the function will return an empty string.
|
||||||
|
// It will read the module hash from the MANIFEST.txt in the [[plugins.FS]] of the provided plugin.
|
||||||
|
// If childFSBase is provided, the function will try to get the hash from MANIFEST.txt for the provided children's
|
||||||
|
// module.js file, rather than for the provided plugin.
|
||||||
|
func (s *Service) moduleHash(ctx context.Context, p pluginstore.Plugin, childFSBase string) (r string, err error) {
|
||||||
|
if !s.cfg.Features.SriChecksEnabled {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore unsigned plugins
|
||||||
|
if !p.Signature.IsValid() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Parent != nil {
|
||||||
|
// Nested plugin
|
||||||
|
parent, ok := s.store.Plugin(ctx, p.Parent.ID)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("parent plugin plugin %q for child plugin %q not found", p.Parent.ID, p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The module hash is contained within the parent's MANIFEST.txt file.
|
||||||
|
// For example, the parent's MANIFEST.txt will contain an entry similar to this:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// "datasource/module.js": "1234567890abcdef..."
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Recursively call moduleHash with the parent plugin and with the children plugin folder path
|
||||||
|
// to get the correct module hash for the nested plugin.
|
||||||
|
if childFSBase == "" {
|
||||||
|
childFSBase = p.Base()
|
||||||
|
}
|
||||||
|
return s.moduleHash(ctx, parent, childFSBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only CDN plugins are supported for SRI checks.
|
||||||
|
// CDN plugins have the version as part of the URL, which acts as a cache-buster.
|
||||||
|
// Needed due to: https://github.com/grafana/plugin-tools/pull/1426
|
||||||
|
// FS plugins build before this change will have SRI mismatch issues.
|
||||||
|
if !s.cdnEnabled(p.ID, p.FS) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := s.signature.ReadPluginManifestFromFS(ctx, p.FS)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read plugin manifest: %w", err)
|
||||||
|
}
|
||||||
|
if !manifest.IsV2() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var childPath string
|
||||||
|
if childFSBase != "" {
|
||||||
|
// Calculate the relative path of the child plugin folder from the parent plugin folder.
|
||||||
|
childPath, err = p.FS.Rel(childFSBase)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("rel path: %w", err)
|
||||||
|
}
|
||||||
|
// MANIFETS.txt uses forward slashes as path separators.
|
||||||
|
childPath = filepath.ToSlash(childPath)
|
||||||
|
}
|
||||||
|
moduleHash, ok := manifest.Files[path.Join(childPath, "module.js")]
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return convertHashForSRI(moduleHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
||||||
@@ -103,3 +188,17 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
|||||||
func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool {
|
func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool {
|
||||||
return s.cdn.PluginSupported(pluginID) || fs.Type().CDN()
|
return s.cdn.PluginSupported(pluginID) || fs.Type().CDN()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks.
|
||||||
|
func convertHashForSRI(h string) (string, error) {
|
||||||
|
hb, err := hex.DecodeString(h)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("hex decode string: %w", err)
|
||||||
|
}
|
||||||
|
return "sha256-" + base64.StdEncoding.EncodeToString(hb), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// moduleHashCacheKey returns a unique key for the module hash cache.
|
||||||
|
func (s *Service) moduleHashCacheKey(p pluginstore.Plugin) string {
|
||||||
|
return p.ID + ":" + p.Info.Version
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ package pluginassets
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
)
|
)
|
||||||
@@ -174,6 +179,349 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestService_ModuleHash(t *testing.T) {
|
||||||
|
const (
|
||||||
|
pluginID = "grafana-test-datasource"
|
||||||
|
parentPluginID = "grafana-test-app"
|
||||||
|
)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
features *config.Features
|
||||||
|
store []pluginstore.Plugin
|
||||||
|
|
||||||
|
// Can be used to configure plugin's fs
|
||||||
|
// fs cdn type = loaded from CDN with no files on disk
|
||||||
|
// fs local type = files on disk but served from CDN only if cdn=true
|
||||||
|
plugin pluginstore.Plugin
|
||||||
|
|
||||||
|
// When true, set cdn=true in config
|
||||||
|
cdn bool
|
||||||
|
expModuleHash string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unsigned should not return module hash",
|
||||||
|
plugin: newPlugin(pluginID, withSignatureStatus(plugins.SignatureStatusUnsigned)),
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: false},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||||
|
withClass(plugins.ClassExternal),
|
||||||
|
),
|
||||||
|
cdn: true,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||||
|
withClass(plugins.ClassExternal),
|
||||||
|
),
|
||||||
|
cdn: true,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||||
|
),
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||||
|
),
|
||||||
|
cdn: true,
|
||||||
|
features: &config.Features{SriChecksEnabled: false},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||||
|
),
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: false},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// parentPluginID (/)
|
||||||
|
// └── pluginID (/datasource)
|
||||||
|
name: "nested plugin should return module hash from parent MANIFEST.txt",
|
||||||
|
store: []pluginstore.Plugin{
|
||||||
|
newPlugin(
|
||||||
|
parentPluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource"))),
|
||||||
|
withParent(parentPluginID),
|
||||||
|
),
|
||||||
|
cdn: true,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: newSRIHash(t, "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// parentPluginID (/)
|
||||||
|
// └── pluginID (/panels/one)
|
||||||
|
name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt",
|
||||||
|
store: []pluginstore.Plugin{
|
||||||
|
newPlugin(
|
||||||
|
parentPluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))),
|
||||||
|
withParent(parentPluginID),
|
||||||
|
),
|
||||||
|
cdn: true,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// grand-parent-app (/)
|
||||||
|
// ├── parent-datasource (/datasource)
|
||||||
|
// │ └── child-panel (/datasource/panels/one)
|
||||||
|
name: "nested plugin of a nested plugin should return module hash from parent MANIFEST.txt",
|
||||||
|
store: []pluginstore.Plugin{
|
||||||
|
newPlugin(
|
||||||
|
"grand-parent-app",
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested"))),
|
||||||
|
),
|
||||||
|
newPlugin(
|
||||||
|
"parent-datasource",
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource"))),
|
||||||
|
withParent("grand-parent-app"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
plugin: newPlugin(
|
||||||
|
"child-panel",
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))),
|
||||||
|
withParent("parent-datasource"),
|
||||||
|
),
|
||||||
|
cdn: true,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested plugin should not return module hash from parent if it's not registered in the store",
|
||||||
|
store: []pluginstore.Plugin{},
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))),
|
||||||
|
withParent(parentPluginID),
|
||||||
|
),
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing module.js entry from MANIFEST.txt should not return module hash",
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js"))),
|
||||||
|
),
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signed status but missing MANIFEST.txt should not return module hash",
|
||||||
|
plugin: newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt"))),
|
||||||
|
),
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if tc.name == "" {
|
||||||
|
var expS string
|
||||||
|
if tc.expModuleHash == "" {
|
||||||
|
expS = "should not return module hash"
|
||||||
|
} else {
|
||||||
|
expS = "should return module hash"
|
||||||
|
}
|
||||||
|
tc.name = fmt.Sprintf("feature=%v, cdn_config=%v, class=%v %s", tc.features.SriChecksEnabled, tc.cdn, tc.plugin.Class, expS)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var pluginSettings config.PluginSettings
|
||||||
|
if tc.cdn {
|
||||||
|
pluginSettings = config.PluginSettings{
|
||||||
|
pluginID: {
|
||||||
|
"cdn": "true",
|
||||||
|
},
|
||||||
|
parentPluginID: map[string]string{
|
||||||
|
"cdn": "true",
|
||||||
|
},
|
||||||
|
"grand-parent-app": map[string]string{
|
||||||
|
"cdn": "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
features := tc.features
|
||||||
|
if features == nil {
|
||||||
|
features = &config.Features{}
|
||||||
|
}
|
||||||
|
pCfg := &config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: "http://cdn.example.com",
|
||||||
|
PluginSettings: pluginSettings,
|
||||||
|
Features: *features,
|
||||||
|
}
|
||||||
|
svc := ProvideService(
|
||||||
|
pCfg,
|
||||||
|
pluginscdn.ProvideService(pCfg),
|
||||||
|
signature.ProvideService(pCfg, statickey.New()),
|
||||||
|
pluginstore.NewFakePluginStore(tc.store...),
|
||||||
|
)
|
||||||
|
mh := svc.ModuleHash(context.Background(), tc.plugin)
|
||||||
|
require.Equal(t, tc.expModuleHash, mh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_ModuleHash_Cache(t *testing.T) {
|
||||||
|
pCfg := &config.PluginManagementCfg{
|
||||||
|
PluginSettings: config.PluginSettings{},
|
||||||
|
Features: config.Features{SriChecksEnabled: true},
|
||||||
|
}
|
||||||
|
svc := ProvideService(
|
||||||
|
pCfg,
|
||||||
|
pluginscdn.ProvideService(pCfg),
|
||||||
|
signature.ProvideService(pCfg, statickey.New()),
|
||||||
|
pluginstore.NewFakePluginStore(),
|
||||||
|
)
|
||||||
|
const pluginID = "grafana-test-datasource"
|
||||||
|
|
||||||
|
t.Run("cache key", func(t *testing.T) {
|
||||||
|
t.Run("with version", func(t *testing.T) {
|
||||||
|
const pluginVersion = "1.0.0"
|
||||||
|
p := newPlugin(pluginID, withInfo(plugins.Info{Version: pluginVersion}))
|
||||||
|
k := svc.moduleHashCacheKey(p)
|
||||||
|
require.Equal(t, pluginID+":"+pluginVersion, k, "cache key should be correct")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("without version", func(t *testing.T) {
|
||||||
|
p := newPlugin(pluginID)
|
||||||
|
k := svc.moduleHashCacheKey(p)
|
||||||
|
require.Equal(t, pluginID+":", k, "cache key should be correct")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ModuleHash usage", func(t *testing.T) {
|
||||||
|
pV1 := newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withInfo(plugins.Info{Version: "1.0.0"}),
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||||
|
)
|
||||||
|
|
||||||
|
pCfg = &config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: "https://cdn.grafana.com",
|
||||||
|
PluginSettings: config.PluginSettings{
|
||||||
|
pluginID: {
|
||||||
|
"cdn": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Features: config.Features{SriChecksEnabled: true},
|
||||||
|
}
|
||||||
|
svc = ProvideService(
|
||||||
|
pCfg,
|
||||||
|
pluginscdn.ProvideService(pCfg),
|
||||||
|
signature.ProvideService(pCfg, statickey.New()),
|
||||||
|
pluginstore.NewFakePluginStore(),
|
||||||
|
)
|
||||||
|
|
||||||
|
k := svc.moduleHashCacheKey(pV1)
|
||||||
|
|
||||||
|
_, ok := svc.moduleHashCache.Load(k)
|
||||||
|
require.False(t, ok, "cache should initially be empty")
|
||||||
|
|
||||||
|
mhV1 := svc.ModuleHash(context.Background(), pV1)
|
||||||
|
pV1Exp := newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03")
|
||||||
|
require.Equal(t, pV1Exp, mhV1, "returned value should be correct")
|
||||||
|
|
||||||
|
cachedMh, ok := svc.moduleHashCache.Load(k)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, pV1Exp, cachedMh, "cache should contain the returned value")
|
||||||
|
|
||||||
|
t.Run("different version uses different cache key", func(t *testing.T) {
|
||||||
|
pV2 := newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withInfo(plugins.Info{Version: "2.0.0"}),
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
// different fs for different hash
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))),
|
||||||
|
)
|
||||||
|
mhV2 := svc.ModuleHash(context.Background(), pV2)
|
||||||
|
require.NotEqual(t, mhV2, mhV1, "different version should have different hash")
|
||||||
|
require.Equal(t, newSRIHash(t, "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a"), mhV2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cache should be used", func(t *testing.T) {
|
||||||
|
// edit cache directly
|
||||||
|
svc.moduleHashCache.Store(k, "hax")
|
||||||
|
require.Equal(t, "hax", svc.ModuleHash(context.Background(), pV1))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertHashFromSRI(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
hash string
|
||||||
|
expHash string
|
||||||
|
expErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811",
|
||||||
|
expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: "not-a-valid-hash",
|
||||||
|
expErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.hash, func(t *testing.T) {
|
||||||
|
r, err := convertHashForSRI(tc.hash)
|
||||||
|
if tc.expErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expHash, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
|
func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p := pluginstore.Plugin{
|
p := pluginstore.Plugin{
|
||||||
JSONData: plugins.JSONData{
|
JSONData: plugins.JSONData{
|
||||||
@@ -186,6 +534,13 @@ func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Pl
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withInfo(info plugins.Info) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p.Info = info
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
|
func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.FS = fs
|
p.FS = fs
|
||||||
@@ -193,6 +548,13 @@ func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withSignatureStatus(status plugins.SignatureStatus) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p.Signature = status
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
|
func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.Angular = plugins.AngularMeta{Detected: angular}
|
p.Angular = plugins.AngularMeta{Detected: angular}
|
||||||
@@ -200,6 +562,13 @@ func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withParent(parentID string) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p.Parent = &pluginstore.ParentPlugin{ID: parentID}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin {
|
func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.Class = class
|
p.Class = class
|
||||||
@@ -218,3 +587,9 @@ func newPluginSettings(pluginID string, kv map[string]string) config.PluginSetti
|
|||||||
pluginID: kv,
|
pluginID: kv,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSRIHash(t *testing.T, s string) string {
|
||||||
|
r, err := convertHashForSRI(s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ var WireSet = wire.NewSet(
|
|||||||
plugincontext.ProvideBaseService,
|
plugincontext.ProvideBaseService,
|
||||||
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
|
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
|
||||||
plugininstaller.ProvideService,
|
plugininstaller.ProvideService,
|
||||||
pluginassets.ProvideModuleHashCalculator,
|
|
||||||
pluginassets.ProvideService,
|
pluginassets.ProvideService,
|
||||||
pluginchecker.ProvidePreinstall,
|
pluginchecker.ProvidePreinstall,
|
||||||
wire.Bind(new(pluginchecker.Preinstall), new(*pluginchecker.PreinstallImpl)),
|
wire.Bind(new(pluginchecker.Preinstall), new(*pluginchecker.PreinstallImpl)),
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const defaultMatchers = {
|
|||||||
* "Time as X" core component, expects ascending x
|
* "Time as X" core component, expects ascending x
|
||||||
*/
|
*/
|
||||||
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
||||||
private plotInstance: React.RefObject<uPlot>;
|
private plotInstance: React.RefObject<uPlot | null>;
|
||||||
|
|
||||||
constructor(props: GraphNGProps) {
|
constructor(props: GraphNGProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ global.ResizeObserver = jest.fn().mockImplementation((callback) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to assign a mock div to a ref
|
// Helper function to assign a mock div to a ref
|
||||||
function assignMockDivToRef(ref: React.RefObject<HTMLDivElement>, mockDiv: HTMLDivElement) {
|
function assignMockDivToRef(ref: React.RefObject<HTMLDivElement | null>, mockDiv: HTMLDivElement) {
|
||||||
// Use type assertion to bypass readonly restriction in tests
|
// Use type assertion to bypass readonly restriction in tests
|
||||||
(ref as { current: HTMLDivElement | null }).current = mockDiv;
|
(ref as { current: HTMLDivElement | null }).current = mockDiv;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import grafanaTextLogoDarkSvg from 'img/grafana_text_logo_dark.svg';
|
|||||||
import grafanaTextLogoLightSvg from 'img/grafana_text_logo_light.svg';
|
import grafanaTextLogoLightSvg from 'img/grafana_text_logo_light.svg';
|
||||||
|
|
||||||
interface SoloPanelPageLogoProps {
|
interface SoloPanelPageLogoProps {
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
hideLogo?: UrlQueryValue;
|
hideLogo?: UrlQueryValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ interface State {
|
|||||||
|
|
||||||
class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
|
class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
|
||||||
subscription?: Unsubscribable;
|
subscription?: Unsubscribable;
|
||||||
ref: RefObject<HTMLDivElement>;
|
ref: RefObject<HTMLDivElement | null>;
|
||||||
|
|
||||||
constructor(props: TransformationsEditorProps) {
|
constructor(props: TransformationsEditorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const LibraryPanelsView = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
|
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
|
||||||
const abortControllerRef = useRef<AbortController>();
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
useDebounce(
|
useDebounce(
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface Props {}
|
|||||||
|
|
||||||
export const LiveConnectionWarning = memo(function LiveConnectionWarning() {
|
export const LiveConnectionWarning = memo(function LiveConnectionWarning() {
|
||||||
const [show, setShow] = useState<boolean | undefined>(undefined);
|
const [show, setShow] = useState<boolean | undefined>(undefined);
|
||||||
const subscriptionRef = useRef<Unsubscribable>();
|
const subscriptionRef = useRef<Unsubscribable | null>(null);
|
||||||
const styles = useStyles2(getStyle);
|
const styles = useStyles2(getStyle);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function useSearchKeyboardNavigation(
|
|||||||
): ItemSelection {
|
): ItemSelection {
|
||||||
const highlightIndexRef = useRef<ItemSelection>({ x: 0, y: -1 });
|
const highlightIndexRef = useRef<ItemSelection>({ x: 0, y: -1 });
|
||||||
const [highlightIndex, setHighlightIndex] = useState<ItemSelection>({ x: 0, y: -1 });
|
const [highlightIndex, setHighlightIndex] = useState<ItemSelection>({ x: 0, y: -1 });
|
||||||
const urlsRef = useRef<Field>();
|
const urlsRef = useRef<Field | null>(null);
|
||||||
|
|
||||||
// Clear selection when the search results change
|
// Clear selection when the search results change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const SuggestionsInput = ({
|
|||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, inputHeight);
|
const styles = getStyles(theme, inputHeight);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>();
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollRef.current?.scrollTo(0, scrollTop);
|
scrollRef.current?.scrollTo(0, scrollTop);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import checkboxWhitePng from 'img/checkbox_white.png';
|
|||||||
|
|
||||||
import { ALL_VARIABLE_VALUE } from '../../constants';
|
import { ALL_VARIABLE_VALUE } from '../../constants';
|
||||||
|
|
||||||
export interface Props extends React.HTMLProps<HTMLUListElement>, Themeable2 {
|
export interface Props extends Omit<React.HTMLProps<HTMLUListElement>, 'onToggle'>, Themeable2 {
|
||||||
multi: boolean;
|
multi: boolean;
|
||||||
values: VariableOption[];
|
values: VariableOption[];
|
||||||
selectedValues: VariableOption[];
|
selectedValues: VariableOption[];
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ interface CodeEditorProps {
|
|||||||
export const LogsQLCodeEditor = (props: CodeEditorProps) => {
|
export const LogsQLCodeEditor = (props: CodeEditorProps) => {
|
||||||
const { query, datasource, onChange } = props;
|
const { query, datasource, onChange } = props;
|
||||||
|
|
||||||
const monacoRef = useRef<Monaco>();
|
const monacoRef = useRef<Monaco | null>(null);
|
||||||
const disposalRef = useRef<monacoType.IDisposable>();
|
const disposalRef = useRef<monacoType.IDisposable | undefined>(undefined);
|
||||||
|
|
||||||
const onFocus = useCallback(async () => {
|
const onFocus = useCallback(async () => {
|
||||||
disposalRef.current = await reRegisterCompletionProvider(
|
disposalRef.current = await reRegisterCompletionProvider(
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ interface LogsCodeEditorProps {
|
|||||||
export const PPLQueryEditor = (props: LogsCodeEditorProps) => {
|
export const PPLQueryEditor = (props: LogsCodeEditorProps) => {
|
||||||
const { query, datasource, onChange } = props;
|
const { query, datasource, onChange } = props;
|
||||||
|
|
||||||
const monacoRef = useRef<Monaco>();
|
const monacoRef = useRef<Monaco | null>(null);
|
||||||
const disposalRef = useRef<monacoType.IDisposable>();
|
const disposalRef = useRef<monacoType.IDisposable | undefined>(undefined);
|
||||||
|
|
||||||
const onFocus = useCallback(async () => {
|
const onFocus = useCallback(async () => {
|
||||||
disposalRef.current = await reRegisterCompletionProvider(
|
disposalRef.current = await reRegisterCompletionProvider(
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ interface SQLCodeEditorProps {
|
|||||||
export const SQLQueryEditor = (props: SQLCodeEditorProps) => {
|
export const SQLQueryEditor = (props: SQLCodeEditorProps) => {
|
||||||
const { query, datasource, onChange } = props;
|
const { query, datasource, onChange } = props;
|
||||||
|
|
||||||
const monacoRef = useRef<Monaco>();
|
const monacoRef = useRef<Monaco | null>(null);
|
||||||
const disposalRef = useRef<monacoType.IDisposable>();
|
const disposalRef = useRef<monacoType.IDisposable | undefined>(undefined);
|
||||||
|
|
||||||
const onFocus = useCallback(async () => {
|
const onFocus = useCallback(async () => {
|
||||||
disposalRef.current = await reRegisterCompletionProvider(
|
disposalRef.current = await reRegisterCompletionProvider(
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const EDITOR_HEIGHT_OFFSET = 2;
|
|||||||
* Hook that returns function that will set up monaco autocomplete for the label selector
|
* Hook that returns function that will set up monaco autocomplete for the label selector
|
||||||
*/
|
*/
|
||||||
function useAutocomplete(getLabelValues: (label: string) => Promise<string[]>, labels?: string[]) {
|
function useAutocomplete(getLabelValues: (label: string) => Promise<string[]>, labels?: string[]) {
|
||||||
const providerRef = useRef<CompletionProvider>();
|
const providerRef = useRef<CompletionProvider | null>(null);
|
||||||
if (providerRef.current === undefined) {
|
if (providerRef.current === undefined) {
|
||||||
providerRef.current = new CompletionProvider();
|
providerRef.current = new CompletionProvider();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function LokiContextUi(props: LokiContextUiProps) {
|
|||||||
window.localStorage.getItem(SHOULD_INCLUDE_PIPELINE_OPERATIONS) === 'true'
|
window.localStorage.getItem(SHOULD_INCLUDE_PIPELINE_OPERATIONS) === 'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
const timerHandle = useRef<number>();
|
const timerHandle = useRef<number | null>(null);
|
||||||
const previousInitialized = useRef<boolean>(false);
|
const previousInitialized = useRef<boolean>(false);
|
||||||
const previousContextFilters = useRef<ContextFilter[]>([]);
|
const previousContextFilters = useRef<ContextFilter[]>([]);
|
||||||
|
|
||||||
@@ -191,14 +191,18 @@ export function LokiContextUi(props: LokiContextUiProps) {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (timerHandle.current) {
|
||||||
clearTimeout(timerHandle.current);
|
clearTimeout(timerHandle.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [contextFilters, initialized]);
|
}, [contextFilters, initialized]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
if (timerHandle.current) {
|
||||||
clearTimeout(timerHandle.current);
|
clearTimeout(timerHandle.current);
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function TraceQLEditor(props: Props) {
|
|||||||
const onRunQueryRef = useRef(onRunQuery);
|
const onRunQueryRef = useRef(onRunQuery);
|
||||||
onRunQueryRef.current = onRunQuery;
|
onRunQueryRef.current = onRunQuery;
|
||||||
|
|
||||||
const errorTimeoutId = useRef<number>();
|
const errorTimeoutId = useRef<number | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -103,7 +103,9 @@ export function TraceQLEditor(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove previous callback if existing, to prevent squiggles from been shown while the user is still typing
|
// Remove previous callback if existing, to prevent squiggles from been shown while the user is still typing
|
||||||
|
if (errorTimeoutId.current) {
|
||||||
window.clearTimeout(errorTimeoutId.current);
|
window.clearTimeout(errorTimeoutId.current);
|
||||||
|
}
|
||||||
|
|
||||||
const errorNodes = getErrorNodes(model.getValue());
|
const errorNodes = getErrorNodes(model.getValue());
|
||||||
const cursorPosition = changeEvent.changes[0].rangeOffset;
|
const cursorPosition = changeEvent.changes[0].rangeOffset;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export function renderHistogram(
|
export function renderHistogram(
|
||||||
can: React.RefObject<HTMLCanvasElement>,
|
can: React.RefObject<HTMLCanvasElement | null>,
|
||||||
histCanWidth: number,
|
histCanWidth: number,
|
||||||
histCanHeight: number,
|
histCanHeight: number,
|
||||||
xVals: number[],
|
xVals: number[],
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export const LogsPanel = ({
|
|||||||
const dataSourcesMap = useDatasourcesFromTargets(panelData.request?.targets);
|
const dataSourcesMap = useDatasourcesFromTargets(panelData.request?.targets);
|
||||||
// Prevents the scroll position to change when new data from infinite scrolling is received
|
// Prevents the scroll position to change when new data from infinite scrolling is received
|
||||||
const keepScrollPositionRef = useRef<null | 'infinite-scroll' | 'user'>(null);
|
const keepScrollPositionRef = useRef<null | 'infinite-scroll' | 'user'>(null);
|
||||||
let closeCallback = useRef<() => void>();
|
const closeCallback = useRef<(() => void) | null>(null);
|
||||||
const { app, eventBus, onAddAdHocFilter } = usePanelContext();
|
const { app, eventBus, onAddAdHocFilter } = usePanelContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function useLayout(
|
|||||||
const currentSignature = createDataSignature(rawNodes, rawEdges);
|
const currentSignature = createDataSignature(rawNodes, rawEdges);
|
||||||
|
|
||||||
const isMounted = useMountedState();
|
const isMounted = useMountedState();
|
||||||
const layoutWorkerCancelRef = useRef<(() => void) | undefined>();
|
const layoutWorkerCancelRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
useUnmount(() => {
|
useUnmount(() => {
|
||||||
if (layoutWorkerCancelRef.current) {
|
if (layoutWorkerCancelRef.current) {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export const AnnotationsPlugin2 = ({
|
|||||||
const newRangeRef = useRef(newRange);
|
const newRangeRef = useRef(newRange);
|
||||||
newRangeRef.current = newRange;
|
newRangeRef.current = newRange;
|
||||||
|
|
||||||
const xAxisRef = useRef<HTMLDivElement>();
|
const xAxisRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
config.addHook('ready', (u) => {
|
config.addHook('ready', (u) => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const ExemplarsPlugin = ({
|
|||||||
maxHeight,
|
maxHeight,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
}: ExemplarsPluginProps) => {
|
}: ExemplarsPluginProps) => {
|
||||||
const plotInstance = useRef<uPlot>();
|
const plotInstance = useRef<uPlot | null>(null);
|
||||||
|
|
||||||
const [lockedExemplarRowIndex, setLockedExemplarRowIndex] = useState<number | undefined>();
|
const [lockedExemplarRowIndex, setLockedExemplarRowIndex] = useState<number | undefined>();
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface ThresholdControlsPluginProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const OutsideRangePlugin = ({ config, onChangeTimeRange }: ThresholdControlsPluginProps) => {
|
export const OutsideRangePlugin = ({ config, onChangeTimeRange }: ThresholdControlsPluginProps) => {
|
||||||
const plotInstance = useRef<uPlot>();
|
const plotInstance = useRef<uPlot | null>(null);
|
||||||
const [timevalues, setTimeValues] = useState<number[] | TypedArray>([]);
|
const [timevalues, setTimeValues] = useState<number[] | TypedArray>([]);
|
||||||
const [timeRange, setTimeRange] = useState<Scale | undefined>();
|
const [timeRange, setTimeRange] = useState<Scale | undefined>();
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface ThresholdControlsPluginProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ThresholdControlsPlugin = ({ config, fieldConfig, onThresholdsChange }: ThresholdControlsPluginProps) => {
|
export const ThresholdControlsPlugin = ({ config, fieldConfig, onThresholdsChange }: ThresholdControlsPluginProps) => {
|
||||||
const plotInstance = useRef<uPlot>();
|
const plotInstance = useRef<uPlot | null>(null);
|
||||||
const [renderToken, setRenderToken] = useState(0);
|
const [renderToken, setRenderToken] = useState(0);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user