iframe 内嵌 grafana

2025-05-24 19:44:11

iframe 内嵌 grafana 时存在状态无法保存以及界面显示不符合预期的情况,本文主要解决该问题。

遇到的问题

  1. 无法保存最后的状态,如果标签切出再进入,则状态丢失,期望保留;
  2. 按 esc 键会退出 kiosk 模式;

解决状态保存问题

解决状态保存有两个办法:

  1. 通过 keepalive 类组件,保存状态;
  2. 记住上次退出时的 url;

首先,我使用的是 react + antd,keepalive 组件确认对 iframe 无效,且无法解决;所以只能用方案二; 使用方案二,一般需要这样的代码:

1
2
3
4
5
6
grafana.contentWindow?.addEventListener('unload', () => {
  const value = grafana.contentWindow?.location.href;
  if (!value) {
    return;
  }
});

然后发现,如果跨域就无法读取 href,即使是不同的子域名也不行,域名必须完全一样,且跨域相关 header 对 iframe 无效,如果要域名完全一样,那么 grafana 只能通过子路劲请求,比如:http://localhost:3000/loongGrafana

通过子路径反代 grafana

修改 /etc/grafana/grafana.ini 文件,具体修改内容如下:

1
2
root_url = %(protocol)s://%(domain)s:%(http_port)s/loongGrafana
serve_from_sub_path = true

修改前,grafana 通过 http://localhost:3000 访问,修改后通过 http://localhost:3000/loongGrafana

grafana 解决 esc 键退出 kiosk 的问题

该问题好解决,js 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
grafana.contentWindow?.addEventListener(
  'keydown',
  (event) => {
    const value = grafana.contentWindow?.location.href;
    if (!value || value.includes('viewPanel=')) {
      return;
    }
    if (event.code === 'Escape' || event.code === 'f') {
      event.preventDefault();
      event.stopPropagation();
      return false;
    }
  },
  true,
);

这里,之所以在 url 中有 viewPanel 时,不禁用 esc 是因为面板可以放大,放大后,可以通过按 esc 键退出,这个不能仅用,放大面板,可以通过快捷键 v 来完成;完整的 js 代码如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
/* eslint-disable react/iframe-missing-sandbox */
import React, { useEffect, useState } from 'react';
import styles from './index.less';

const fixUrl = (value: string) => {
  if (value.endsWith('&kiosk')) {
    return value;
  }

  const arr = value.split('?');
  if (arr.length != 2) {
    return null;
  }

  let url = arr[0];
  let params = arr[1];
  const tmpArr = params.split('&');
  const size = tmpArr.length;
  let tmp = '';
  for (let index = 0; index < size; index++) {
    const tmpParams = tmpArr[index];
    if (tmpParams.startsWith('kiosk')) {
      continue;
    }
    tmp += '&' + tmpParams;
  }
  url += '?kiosk' + tmp;
  return url;
};

const Grafana: React.FC = () => {
  const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
  const [url, setUrl] = useState<string>('');
  const [hasListener, setHasListener] = useState<boolean>(false);

  useEffect(() => {
    localStorage.setItem('SearchBar_Hidden', 'true');
    localStorage.setItem('grafana.navigation.docked', 'false');
    let value = localStorage.getItem('grafanaUrl');
    if (value) {
      value = fixUrl(value);
    }
    const host = window.location.protocol + '//' + window.location.hostname;
    const url = host + '/loongGrafana/d/bKSMHLaIk/loong';
    if (value != null && value.startsWith(url)) {
      setUrl(value);
    } else {
      setUrl(url + '?theme=light&kiosk');
    }
  }, []);

  const loadFinish = () => {
    const grafana = iframeRef.current;
    if (!grafana) {
      return;
    }
    if (hasListener) {
      return;
    }
    grafana.contentWindow?.addEventListener('unload', () => {
      const value = grafana.contentWindow?.location.href;
      if (!value) {
        return;
      }
      const url = fixUrl(value);
      const prefix = window.location.protocol + '//' + window.location.hostname + '/loongGrafana/d/bKSMHLaIk/loong';
      if (url && url.startsWith(prefix)) {
        localStorage.setItem('grafanaUrl', url);
      }
    });

    grafana.contentWindow?.addEventListener(
      'keydown',
      (event) => {
        const value = grafana.contentWindow?.location.href;
        if (!value || value.includes('viewPanel=')) {
          return;
        }
        if (event.code === 'Escape' || event.code === 'f') {
          event.preventDefault();
          event.stopPropagation();
          return false;
        }
      },
      true,
    );
    setHasListener(true);
  };

  return (
    <div className={styles.container}>
      <div className={styles.content}>
        <iframe
          id="grafana"
          ref={iframeRef}
          onLoad={() => loadFinish()}
          style={{ width: '100%', height: '100%', display: 'flex', flex: '1', border: '0' }}
          src={url}
        />
      </div>
    </div>
  );
};

export default Grafana;
最后更新于