diff --git a/.github/workflows/bundle-types.yml b/.github/workflows/bundle-types.yml
new file mode 100644
index 0000000..74d9db0
--- /dev/null
+++ b/.github/workflows/bundle-types.yml
@@ -0,0 +1,25 @@
+name: Bundle Types
+
+on:
+ push:
+ tags:
+ - 'v*' # Run workflow on version tags, e.g. v1.0.0.
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# These permissions are needed to assume roles from Github's OIDC.
+permissions:
+ contents: read
+ id-token: write
+
+jobs:
+ bundle-types:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: grafana/plugin-actions/bundle-types@main
+ with:
+ entry-point: ./src/externalComponents/types.ts
+ ts-config: ./tsconfig-for-bundle-types.json
\ No newline at end of file
diff --git a/src/exposedComponents/OpenInExploreTracesButton/OpenInExploreTracesButton.tsx b/src/exposedComponents/OpenInExploreTracesButton/OpenInExploreTracesButton.tsx
new file mode 100644
index 0000000..c1f1fa7
--- /dev/null
+++ b/src/exposedComponents/OpenInExploreTracesButton/OpenInExploreTracesButton.tsx
@@ -0,0 +1,56 @@
+import { useReturnToPrevious } from '@grafana/runtime';
+import { LinkButton } from '@grafana/ui';
+import React, { useMemo } from 'react';
+import { OpenInExploreTracesButtonProps } from '../types';
+import pluginJson from '../../plugin.json';
+
+export default function OpenInExploreTracesButton({
+ datasourceUid,
+ matchers,
+ from,
+ to,
+ returnToPreviousSource,
+ renderButton,
+}: OpenInExploreTracesButtonProps) {
+ const setReturnToPrevious = useReturnToPrevious();
+
+ const href = useMemo(() => {
+ let params = new URLSearchParams();
+
+ if (datasourceUid) {
+ params.append('var-ds', datasourceUid);
+ }
+
+ if (from) {
+ params.append('from', from);
+ }
+
+ if (to) {
+ params.append('to', to);
+ }
+
+ matchers.forEach((streamSelector) => {
+ params.append('var-filters', `${streamSelector.name}|${streamSelector.operator}|${streamSelector.value}`);
+ });
+
+ return `a/${pluginJson.id}/explore?${params.toString()}`;
+ }, [datasourceUid, from, to, matchers]);
+
+ if (!href) {
+ return null;
+ }
+
+ if (renderButton) {
+ return renderButton({ href });
+ }
+
+ return (
+ setReturnToPrevious(returnToPreviousSource || 'previous')}
+ >
+ Open in Traces drilldown
+
+ );
+}
diff --git a/src/exposedComponents/index.tsx b/src/exposedComponents/index.tsx
new file mode 100644
index 0000000..f904861
--- /dev/null
+++ b/src/exposedComponents/index.tsx
@@ -0,0 +1,27 @@
+import { LinkButton } from '@grafana/ui';
+import { OpenInExploreTracesButtonProps } from 'exposedComponents/types';
+import React, { lazy, Suspense } from 'react';
+const OpenInExploreTracesButton = lazy(() => import('exposedComponents/OpenInExploreTracesButton/OpenInExploreTracesButton'));
+
+function SuspendedOpenInExploreTracesButton(props: OpenInExploreTracesButtonProps) {
+ return (
+
+ Open in Traces drilldown
+
+ }
+ >
+
+
+ );
+}
+
+export const exposedComponents = [
+ {
+ id: 'grafana-exploretraces-app/open-in-explore-traces-button/v1',
+ title: 'Open in Traces drilldown button',
+ description: 'A button that opens a traces view in the Traces drilldown app.',
+ component: SuspendedOpenInExploreTracesButton,
+ },
+];
diff --git a/src/exposedComponents/types.ts b/src/exposedComponents/types.ts
new file mode 100644
index 0000000..6a3ec32
--- /dev/null
+++ b/src/exposedComponents/types.ts
@@ -0,0 +1,15 @@
+
+export type TempoMatcher = {
+ name: string;
+ value: string;
+ operator: '=' | '!=' | '>' | '<';
+};
+
+export interface OpenInExploreTracesButtonProps {
+ datasourceUid?: string;
+ matchers: TempoMatcher[];
+ from?: string;
+ to?: string;
+ returnToPreviousSource?: string;
+ renderButton?: (props: { href: string }) => React.ReactElement;
+}
diff --git a/src/module.tsx b/src/module.tsx
index 102441c..da147ab 100644
--- a/src/module.tsx
+++ b/src/module.tsx
@@ -4,6 +4,7 @@ import { AppPlugin } from '@grafana/data';
// @ts-ignore new API that is not yet in stable release
import { sidecarServiceSingleton_EXPERIMENTAL } from '@grafana/runtime';
import pluginJson from './plugin.json';
+import { exposedComponents } from 'exposedComponents';
const App = lazy(() => import('./components/App/App'));
const AppConfig = lazy(() => import('./components/AppConfig/AppConfig'));
@@ -23,3 +24,8 @@ export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({
sidecarServiceSingleton_EXPERIMENTAL?.openApp(pluginJson.id, helpers.context);
},
});
+
+
+for (const exposedComponentConfig of exposedComponents) {
+ plugin.exposeComponent(exposedComponentConfig);
+}
diff --git a/src/plugin.json b/src/plugin.json
index ffb00ba..365f1d7 100644
--- a/src/plugin.json
+++ b/src/plugin.json
@@ -57,6 +57,13 @@
"plugins": []
},
"extensions": {
+ "exposedComponents": [
+ {
+ "id": "grafana-exploretraces-app/open-in-explore-traces-button/v1",
+ "title": "Open in Traces drilldown button",
+ "description": "A button that opens a traces view in the Traces drilldown app."
+ }
+ ],
"addedLinks": [
{
"targets": ["grafana-lokiexplore-app/toolbar-open-related/v1"],
diff --git a/tsconfig-for-bundle-types.json b/tsconfig-for-bundle-types.json
new file mode 100644
index 0000000..5a37ca0
--- /dev/null
+++ b/tsconfig-for-bundle-types.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "jsx": "react",
+ "resolveJsonModule": true
+ }
+}
\ No newline at end of file