mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 16:53:19 +08:00
Explore: Show links to queryless apps (#96625)
* Extract basic extensions to a separate files * Add simple queryless apps links * Move links for queryless apps next to the datasource picker * Update tests * Add translations * Add tracking * Update translations * Fix tests and betterer * Fix the mock for the test (the hook may be called twice now) * Add a todo
This commit is contained in:
@ -3148,9 +3148,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/explore/extensions/ToolbarExtensionPoint.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/explore/hooks/useStateSync/index.ts:5381": [
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./external.utils\`)", "0"]
|
||||
],
|
||||
|
@ -180,7 +180,7 @@ describe('Explore', () => {
|
||||
});
|
||||
|
||||
it('should render toolbar extension point if extensions is available', async () => {
|
||||
usePluginLinksMock.mockReturnValueOnce({
|
||||
usePluginLinksMock.mockReturnValue({
|
||||
links: [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -255,6 +255,12 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
|
||||
hideTextValue={showSmallDataSourcePicker}
|
||||
width={showSmallDataSourcePicker ? 8 : undefined}
|
||||
/>,
|
||||
<ToolbarExtensionPoint
|
||||
key="toolbar-extension-point"
|
||||
exploreId={exploreId}
|
||||
timeZone={timeZone}
|
||||
extensionsToShow="queryless"
|
||||
/>,
|
||||
].filter(Boolean)}
|
||||
forceShowLeftItems
|
||||
>
|
||||
@ -295,7 +301,12 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
|
||||
</ToolbarButton>
|
||||
</ButtonGroup>
|
||||
),
|
||||
<ToolbarExtensionPoint key="toolbar-extension-point" exploreId={exploreId} timeZone={timeZone} />,
|
||||
<ToolbarExtensionPoint
|
||||
key="toolbar-extension-point"
|
||||
exploreId={exploreId}
|
||||
timeZone={timeZone}
|
||||
extensionsToShow="basic"
|
||||
/>,
|
||||
!isLive && (
|
||||
<ExploreTimeControls
|
||||
key="timeControls"
|
||||
|
@ -51,6 +51,18 @@ function renderWithExploreStore(
|
||||
render(<Provider store={store}>{children}</Provider>, {});
|
||||
}
|
||||
|
||||
function setupToolbarExtensionPoint(
|
||||
{ noTimezone, showQuerylessApps }: { noTimezone?: boolean; showQuerylessApps?: boolean } = { noTimezone: false }
|
||||
) {
|
||||
return (
|
||||
<ToolbarExtensionPoint
|
||||
exploreId="left"
|
||||
timeZone={noTimezone ? '' : 'browser'}
|
||||
extensionsToShow={showQuerylessApps ? 'queryless' : 'basic'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
describe('ToolbarExtensionPoint', () => {
|
||||
describe('with extension points', () => {
|
||||
beforeAll(() => {
|
||||
@ -79,13 +91,13 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
|
||||
it('should render "Add" extension point menu button', () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Add' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render menu with extensions when "Add" is clicked', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
@ -95,7 +107,7 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
|
||||
it('should call onClick from extension when menu item is clicked', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Add to dashboard' }));
|
||||
@ -109,7 +121,7 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
|
||||
it('should render confirm navigation modal when extension with path is clicked', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
await userEvent.click(screen.getByRole('menuitem', { name: 'ML: Forecast' }));
|
||||
@ -123,7 +135,7 @@ describe('ToolbarExtensionPoint', () => {
|
||||
const targets = [{ refId: 'A' }];
|
||||
const data = createEmptyQueryResponse();
|
||||
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />, {
|
||||
renderWithExploreStore(setupToolbarExtensionPoint(), {
|
||||
targets,
|
||||
data,
|
||||
});
|
||||
@ -148,7 +160,7 @@ describe('ToolbarExtensionPoint', () => {
|
||||
const targets = [{ refId: 'A' }];
|
||||
const data = createEmptyQueryResponse();
|
||||
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="" />, {
|
||||
renderWithExploreStore(setupToolbarExtensionPoint({ noTimezone: true }), {
|
||||
targets,
|
||||
data,
|
||||
});
|
||||
@ -160,7 +172,7 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
|
||||
it('should correct extension point id when fetching extensions', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
const [options] = usePluginLinksMock.mock.calls[0];
|
||||
const { extensionPointId } = options;
|
||||
@ -195,13 +207,13 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
|
||||
it('should render "Add" extension point menu button', () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Add' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render menu with extensions when "Add" is clicked', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
@ -219,7 +231,7 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
|
||||
it('should render "add to dashboard" action button if one pane is visible', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
await waitFor(() => {
|
||||
const button = screen.getByRole('button', { name: /add to dashboard/i });
|
||||
@ -237,9 +249,119 @@ describe('ToolbarExtensionPoint', () => {
|
||||
});
|
||||
|
||||
it('should not render "add to dashboard" action button', async () => {
|
||||
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
expect(screen.queryByRole('button', { name: /add to dashboard/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with multiple queryless apps links', () => {
|
||||
beforeAll(() => {
|
||||
usePluginLinksMock.mockReturnValue({
|
||||
links: [
|
||||
{
|
||||
pluginId: 'grafana',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Add to dashboard',
|
||||
category: 'Dashboards',
|
||||
description: 'Add the current query as a panel to a dashboard',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '2',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'ML: Forecast',
|
||||
description: 'Add the query as a ML forecast',
|
||||
path: '/a/grafana-ml-ap/forecast',
|
||||
},
|
||||
{
|
||||
pluginId: 'grafana-pyroscope-app',
|
||||
id: '3',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Explore Profiles',
|
||||
description: 'Explore Profiles',
|
||||
path: '/a/grafana-pyroscope-app',
|
||||
},
|
||||
{
|
||||
pluginId: 'grafana-lokiexplore-app',
|
||||
id: '4',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Explore Logs',
|
||||
description: 'Explore Logs',
|
||||
path: '/a/grafana-lokiexplore-app',
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render menu with extensions without queryless apps when "Add" is clicked', async () => {
|
||||
renderWithExploreStore(setupToolbarExtensionPoint());
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
expect(screen.getByRole('group', { name: 'Dashboards' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'Add to dashboard' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible();
|
||||
expect(screen.queryByRole('menuitem', { name: 'Explore Profiles' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render queryless apps links', async () => {
|
||||
renderWithExploreStore(setupToolbarExtensionPoint({ showQuerylessApps: true }));
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /go queryless/i }));
|
||||
|
||||
expect(screen.queryByRole('group', { name: 'Dashboards' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('menuitem', { name: 'Add to dashboard' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('menuitem', { name: 'ML: Forecast' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', { name: 'Explore Profiles' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: 'Explore Logs' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with single queryless apps link', () => {
|
||||
beforeAll(() => {
|
||||
usePluginLinksMock.mockReturnValue({
|
||||
links: [
|
||||
{
|
||||
pluginId: 'grafana',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Add to dashboard',
|
||||
category: 'Dashboards',
|
||||
description: 'Add the current query as a panel to a dashboard',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '2',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'ML: Forecast',
|
||||
description: 'Add the query as a ML forecast',
|
||||
path: '/a/grafana-ml-ap/forecast',
|
||||
},
|
||||
{
|
||||
pluginId: 'grafana-pyroscope-app',
|
||||
id: '3',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Explore Profiles',
|
||||
description: 'Explore Profiles',
|
||||
path: '/a/grafana-pyroscope-app',
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render single queryless app link', async () => {
|
||||
renderWithExploreStore(setupToolbarExtensionPoint({ showQuerylessApps: true }));
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /go queryless/i }));
|
||||
|
||||
expect(screen.queryByRole('menuitem', { name: 'Explore Profiles' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('menuitem', { name: 'Explore Logs' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,66 +1,69 @@
|
||||
import { lazy, ReactElement, Suspense, useMemo, useState } from 'react';
|
||||
import { ReactElement, useMemo, useState } from 'react';
|
||||
|
||||
import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange, getTimeZone } from '@grafana/data';
|
||||
import { config, usePluginLinks } from '@grafana/runtime';
|
||||
import { config, reportInteraction, usePluginLinks } from '@grafana/runtime';
|
||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||
import { Dropdown, ToolbarButton } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction, ExplorePanelData, useSelector } from 'app/types';
|
||||
|
||||
import { getExploreItemSelector, isLeftPaneSelector, selectCorrelationDetails } from '../state/selectors';
|
||||
|
||||
import { ConfirmNavigationModal } from './ConfirmNavigationModal';
|
||||
import { ToolbarExtensionPointMenu } from './ToolbarExtensionPointMenu';
|
||||
|
||||
const AddToDashboard = lazy(() =>
|
||||
import('./AddToDashboard').then(({ AddToDashboard }) => ({ default: AddToDashboard }))
|
||||
);
|
||||
import { BasicExtensions } from './toolbar/BasicExtensions';
|
||||
import { QuerylessAppsExtensions } from './toolbar/QuerylessAppsExtensions';
|
||||
|
||||
type Props = {
|
||||
exploreId: string;
|
||||
timeZone: TimeZone;
|
||||
extensionsToShow: 'queryless' | 'basic';
|
||||
};
|
||||
|
||||
const QUERYLESS_APPS = ['grafana-pyroscope-app', 'grafana-lokiexplore-app', 'grafana-exploretraces-app'];
|
||||
|
||||
export function ToolbarExtensionPoint(props: Props): ReactElement | null {
|
||||
const { exploreId } = props;
|
||||
const { exploreId, extensionsToShow } = props;
|
||||
const [selectedExtension, setSelectedExtension] = useState<PluginExtensionLink | undefined>();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const context = useExtensionPointContext(props);
|
||||
// TODO: Pull it up to avoid calling it twice
|
||||
const { links } = usePluginLinks({
|
||||
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
|
||||
context: context,
|
||||
limitPerPlugin: 3,
|
||||
});
|
||||
const selectExploreItem = getExploreItemSelector(exploreId);
|
||||
const noQueriesInPane = useSelector(selectExploreItem)?.queries?.length;
|
||||
const noQueriesInPane = Boolean(useSelector(selectExploreItem)?.queries?.length);
|
||||
|
||||
// If we only have the explore core extension point registered we show the old way of
|
||||
// adding a query to a dashboard.
|
||||
if (links.length <= 1) {
|
||||
const canAddPanelToDashboard =
|
||||
contextSrv.hasPermission(AccessControlAction.DashboardsCreate) ||
|
||||
contextSrv.hasPermission(AccessControlAction.DashboardsWrite);
|
||||
|
||||
if (!canAddPanelToDashboard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<AddToDashboard exploreId={exploreId} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const menu = <ToolbarExtensionPointMenu extensions={links} onSelect={setSelectedExtension} />;
|
||||
const querylessLinks = links.filter((link) => QUERYLESS_APPS.includes(link.pluginId));
|
||||
const commonLinks = links.filter((link) => !QUERYLESS_APPS.includes(link.pluginId));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown onVisibleChange={setIsOpen} placement="bottom-start" overlay={menu}>
|
||||
<ToolbarButton aria-label="Add" disabled={!Boolean(noQueriesInPane)} variant="canvas" isOpen={isOpen}>
|
||||
Add
|
||||
</ToolbarButton>
|
||||
</Dropdown>
|
||||
{extensionsToShow === 'queryless' && (
|
||||
<QuerylessAppsExtensions
|
||||
links={querylessLinks}
|
||||
noQueriesInPane={noQueriesInPane}
|
||||
exploreId={exploreId}
|
||||
setSelectedExtension={(extension) => {
|
||||
setSelectedExtension(extension);
|
||||
reportInteraction('grafana_explore_queryless_app_link_clicked', {
|
||||
pluginId: extension.pluginId,
|
||||
});
|
||||
}}
|
||||
setIsModalOpen={setIsOpen}
|
||||
isModalOpen={isOpen}
|
||||
/>
|
||||
)}
|
||||
{extensionsToShow === 'basic' && (
|
||||
<BasicExtensions
|
||||
links={commonLinks}
|
||||
noQueriesInPane={noQueriesInPane}
|
||||
exploreId={exploreId}
|
||||
setSelectedExtension={setSelectedExtension}
|
||||
setIsModalOpen={setIsOpen}
|
||||
isModalOpen={isOpen}
|
||||
/>
|
||||
)}
|
||||
{!!selectedExtension && !!selectedExtension.path && (
|
||||
<ConfirmNavigationModal
|
||||
path={selectedExtension.path}
|
||||
|
@ -0,0 +1,47 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { Dropdown, ToolbarButton } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { Trans } from '../../../../core/internationalization';
|
||||
import { ToolbarExtensionPointMenu } from '../ToolbarExtensionPointMenu';
|
||||
|
||||
import { ExtensionDropdownProps } from './types';
|
||||
|
||||
const AddToDashboard = lazy(() =>
|
||||
import('./../AddToDashboard').then(({ AddToDashboard }) => ({ default: AddToDashboard }))
|
||||
);
|
||||
|
||||
export function BasicExtensions(props: ExtensionDropdownProps) {
|
||||
const { exploreId, links, setSelectedExtension, setIsModalOpen, isModalOpen, noQueriesInPane } = props;
|
||||
// If we only have the explore core extension point registered we show the old way of
|
||||
// adding a query to a dashboard.
|
||||
if (links.length <= 1) {
|
||||
const canAddPanelToDashboard =
|
||||
contextSrv.hasPermission(AccessControlAction.DashboardsCreate) ||
|
||||
contextSrv.hasPermission(AccessControlAction.DashboardsWrite);
|
||||
|
||||
if (!canAddPanelToDashboard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<AddToDashboard exploreId={exploreId} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const menu = <ToolbarExtensionPointMenu extensions={links} onSelect={setSelectedExtension} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown onVisibleChange={setIsModalOpen} placement="bottom-start" overlay={menu}>
|
||||
<ToolbarButton aria-label="Add" disabled={!Boolean(noQueriesInPane)} variant="canvas" isOpen={isModalOpen}>
|
||||
<Trans i18nKey="explore.toolbar.add-to-extensions">Add</Trans>
|
||||
</ToolbarButton>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { first } from 'lodash';
|
||||
|
||||
import { Dropdown, ToolbarButton } from '@grafana/ui';
|
||||
|
||||
import { Trans } from '../../../../core/internationalization';
|
||||
import { ToolbarExtensionPointMenu } from '../ToolbarExtensionPointMenu';
|
||||
|
||||
import { ExtensionDropdownProps } from './types';
|
||||
|
||||
export function QuerylessAppsExtensions(props: ExtensionDropdownProps) {
|
||||
const { links, setSelectedExtension, setIsModalOpen, isModalOpen, noQueriesInPane } = props;
|
||||
|
||||
if (links.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const menu = <ToolbarExtensionPointMenu extensions={links} onSelect={setSelectedExtension} />;
|
||||
|
||||
if (links.length === 1) {
|
||||
const link = first(links)!;
|
||||
return (
|
||||
<ToolbarButton variant="canvas" icon={link.icon} onClick={() => setSelectedExtension(link)}>
|
||||
<Trans i18nKey="explore.toolbar.add-to-queryless-extensions">Go queryless</Trans>
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown onVisibleChange={setIsModalOpen} placement="bottom-start" overlay={menu}>
|
||||
<ToolbarButton
|
||||
aria-label="Go Queryless"
|
||||
disabled={!Boolean(noQueriesInPane)}
|
||||
variant="canvas"
|
||||
isOpen={isModalOpen}
|
||||
>
|
||||
<Trans i18nKey="explore.toolbar.add-to-queryless-extensions">Go queryless</Trans>
|
||||
</ToolbarButton>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
}
|
10
public/app/features/explore/extensions/toolbar/types.ts
Normal file
10
public/app/features/explore/extensions/toolbar/types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { PluginExtensionLink } from '@grafana/data';
|
||||
|
||||
export type ExtensionDropdownProps = {
|
||||
links: PluginExtensionLink[];
|
||||
exploreId: string;
|
||||
setSelectedExtension: (extension: PluginExtensionLink) => void;
|
||||
setIsModalOpen: (value: boolean) => void;
|
||||
isModalOpen: boolean;
|
||||
noQueriesInPane: boolean;
|
||||
};
|
@ -1261,6 +1261,8 @@
|
||||
"title-with-name": "Table - {{name}}"
|
||||
},
|
||||
"toolbar": {
|
||||
"add-to-extensions": "Add",
|
||||
"add-to-queryless-extensions": "Go queryless",
|
||||
"aria-label": "Explore toolbar",
|
||||
"copy-link": "Copy URL",
|
||||
"copy-link-abs-time": "Copy absolute URL",
|
||||
|
@ -1261,6 +1261,8 @@
|
||||
"title-with-name": "Ŧäþľę - {{name}}"
|
||||
},
|
||||
"toolbar": {
|
||||
"add-to-extensions": "Åđđ",
|
||||
"add-to-queryless-extensions": "Ğő qūęřyľęşş",
|
||||
"aria-label": "Ēχpľőřę ŧőőľþäř",
|
||||
"copy-link": "Cőpy ŮŖĿ",
|
||||
"copy-link-abs-time": "Cőpy äþşőľūŧę ŮŖĿ",
|
||||
|
Reference in New Issue
Block a user