mirror of https://github.com/grafana/grafana.git
Add favorite button to EditDatasource page (#109609)
This commit is contained in:
parent
06115478f9
commit
f5b9d93610
|
|
@ -10,6 +10,7 @@ const FAVORITE_DATASOURCES_KEY = 'favoriteDatasources';
|
|||
|
||||
export type FavoriteDatasources = {
|
||||
enabled: boolean;
|
||||
isLoading: boolean;
|
||||
favoriteDatasources: string[];
|
||||
initialFavoriteDataSources: string[];
|
||||
addFavoriteDatasource: (ds: DataSourceInstanceSettings) => void;
|
||||
|
|
@ -35,6 +36,7 @@ export function useFavoriteDatasources(): FavoriteDatasources {
|
|||
if (!config.featureToggles.favoriteDatasources) {
|
||||
return {
|
||||
enabled: false,
|
||||
isLoading: false,
|
||||
favoriteDatasources: [],
|
||||
initialFavoriteDataSources: [],
|
||||
addFavoriteDatasource: () => {},
|
||||
|
|
@ -46,16 +48,19 @@ export function useFavoriteDatasources(): FavoriteDatasources {
|
|||
const [userStorage] = useState(() => new UserStorage('grafana-runtime'));
|
||||
const [favoriteDatasources, setFavoriteDatasources] = useState<string[]>([]);
|
||||
const [initialFavoriteDataSources, setInitialFavoriteDataSources] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Load favorites from storage on mount
|
||||
useEffect(() => {
|
||||
const loadFavorites = async () => {
|
||||
setIsLoading(true);
|
||||
const stored = await userStorage.getItem(FAVORITE_DATASOURCES_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
setFavoriteDatasources(parsed);
|
||||
setInitialFavoriteDataSources(parsed);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loadFavorites();
|
||||
|
|
@ -64,8 +69,10 @@ export function useFavoriteDatasources(): FavoriteDatasources {
|
|||
// Helper function to save favorites to storage
|
||||
const saveFavorites = useCallback(
|
||||
async (newFavorites: string[]) => {
|
||||
setIsLoading(true);
|
||||
await userStorage.setItem(FAVORITE_DATASOURCES_KEY, JSON.stringify(newFavorites));
|
||||
setFavoriteDatasources(newFavorites);
|
||||
setIsLoading(false);
|
||||
},
|
||||
[userStorage]
|
||||
);
|
||||
|
|
@ -104,6 +111,7 @@ export function useFavoriteDatasources(): FavoriteDatasources {
|
|||
|
||||
return {
|
||||
enabled: true,
|
||||
isLoading,
|
||||
favoriteDatasources,
|
||||
addFavoriteDatasource,
|
||||
removeFavoriteDatasource,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { DataSourcesListView, ViewProps } from './DataSourcesList';
|
|||
const mockIsFavoriteDatasource = jest.fn();
|
||||
const mockUseFavoriteDatasources = jest.fn(() => ({
|
||||
enabled: true,
|
||||
isLoading: false,
|
||||
isFavoriteDatasource: mockIsFavoriteDatasource,
|
||||
favoriteDatasources: [],
|
||||
initialFavoriteDataSources: [],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { PluginExtensionTypes, IconName } from '@grafana/data';
|
||||
import { setPluginLinksHook } from '@grafana/runtime';
|
||||
import { setPluginLinksHook, config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
import { getMockDataSource } from '../mocks/dataSourcesMocks';
|
||||
|
|
@ -16,11 +16,48 @@ jest.mock('../utils', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
// Mock @grafana/runtime
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
config: {
|
||||
featureToggles: {
|
||||
favoriteDatasources: false,
|
||||
},
|
||||
},
|
||||
getDataSourceSrv: jest.fn(),
|
||||
useFavoriteDatasources: jest.fn(),
|
||||
}));
|
||||
|
||||
// Set default plugin links hook
|
||||
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
||||
|
||||
// Mock contextSrv
|
||||
const mockContextSrv = contextSrv as jest.Mocked<typeof contextSrv>;
|
||||
const mockContextSrv = jest.mocked(contextSrv);
|
||||
|
||||
// Mock getDataSourceSrv and favorite hooks
|
||||
const mockGetDataSourceSrv = jest.mocked(getDataSourceSrv);
|
||||
const mockUseFavoriteDatasources = jest.mocked(require('@grafana/runtime').useFavoriteDatasources);
|
||||
|
||||
// Create mock datasource instance
|
||||
const mockDataSourceInstance = {
|
||||
uid: 'test-uid',
|
||||
name: 'Test Prometheus',
|
||||
type: 'prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
builtIn: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Mock favorite datasources hook return value
|
||||
const mockFavoriteHook = {
|
||||
enabled: true,
|
||||
favoriteDatasources: [],
|
||||
initialFavoriteDataSources: [],
|
||||
isFavoriteDatasource: jest.fn(),
|
||||
addFavoriteDatasource: jest.fn(),
|
||||
removeFavoriteDatasource: jest.fn(),
|
||||
};
|
||||
|
||||
// Helper function to create mock plugin link extensions with all required properties
|
||||
const createMockPluginLink = (
|
||||
|
|
@ -63,6 +100,24 @@ describe('EditDataSourceActions', () => {
|
|||
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
||||
// Default contextSrv mock - user has explore rights
|
||||
mockContextSrv.hasAccessToExplore.mockReturnValue(true);
|
||||
|
||||
// Setup default mocks for favorite functionality
|
||||
mockGetDataSourceSrv.mockReturnValue({
|
||||
getInstanceSettings: jest.fn().mockReturnValue(mockDataSourceInstance),
|
||||
get: jest.fn(),
|
||||
getList: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
registerRuntimeDataSource: jest.fn(),
|
||||
});
|
||||
|
||||
// Reset favorite hook mocks
|
||||
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
||||
mockFavoriteHook.addFavoriteDatasource.mockClear();
|
||||
mockFavoriteHook.removeFavoriteDatasource.mockClear();
|
||||
|
||||
// Default: feature toggle disabled, so no favorite hook
|
||||
mockUseFavoriteDatasources.mockReturnValue({ ...mockFavoriteHook, enabled: false });
|
||||
config.featureToggles.favoriteDatasources = false;
|
||||
});
|
||||
|
||||
describe('Core Actions', () => {
|
||||
|
|
@ -236,7 +291,7 @@ describe('EditDataSourceActions', () => {
|
|||
title: 'Test Action',
|
||||
description: 'Test description',
|
||||
path: '/test-path',
|
||||
icon: 'external-link-alt' as IconName,
|
||||
icon: 'external-link-alt',
|
||||
pluginId: 'grafana-lokiexplore-app',
|
||||
}),
|
||||
];
|
||||
|
|
@ -318,4 +373,123 @@ describe('EditDataSourceActions', () => {
|
|||
expect(screen.getByText('Explore data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Favorite Actions', () => {
|
||||
it('should not render favorite button when feature toggle is disabled', () => {
|
||||
config.featureToggles.favoriteDatasources = false;
|
||||
mockUseFavoriteDatasources.mockReturnValue({ ...mockFavoriteHook, enabled: false });
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
// Should not find any favorite button
|
||||
expect(screen.queryByTestId('favorite-button')).not.toBeInTheDocument();
|
||||
// Core actions should still be rendered
|
||||
expect(screen.getByText('Explore data')).toBeInTheDocument();
|
||||
expect(screen.getByText('Build a dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render favorite button for built-in datasources', () => {
|
||||
config.featureToggles.favoriteDatasources = true;
|
||||
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
||||
|
||||
// Mock built-in datasource
|
||||
const builtInDataSource = { ...mockDataSourceInstance, meta: { ...mockDataSourceInstance.meta, builtIn: true } };
|
||||
mockGetDataSourceSrv.mockReturnValue({
|
||||
getInstanceSettings: jest.fn().mockReturnValue(builtInDataSource),
|
||||
get: jest.fn(),
|
||||
getList: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
registerRuntimeDataSource: jest.fn(),
|
||||
});
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
// Should not find any favorite button for built-in datasources
|
||||
expect(screen.queryByTestId('favorite-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render favorite button when feature toggle is enabled and datasource is not built-in', () => {
|
||||
config.featureToggles.favoriteDatasources = true;
|
||||
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
||||
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
// Should find star icon for non-favorite datasource
|
||||
const favoriteButton = screen.getByTestId('favorite-button');
|
||||
expect(favoriteButton).toBeInTheDocument();
|
||||
|
||||
// Should have correct aria-label for non-favorite datasource
|
||||
expect(favoriteButton).toHaveAttribute('aria-label', 'Add to favorites');
|
||||
});
|
||||
|
||||
it('should show favorite icon when datasource is favorited', () => {
|
||||
config.featureToggles.favoriteDatasources = true;
|
||||
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
||||
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(true);
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
// Should find favorite button for favorited datasource
|
||||
const favoriteButton = screen.getByTestId('favorite-button');
|
||||
expect(favoriteButton).toBeInTheDocument();
|
||||
|
||||
// Should have correct aria-label for favorited datasource
|
||||
expect(favoriteButton).toHaveAttribute('aria-label', 'Remove from favorites');
|
||||
});
|
||||
|
||||
it('should add datasource to favorites when star button is clicked', () => {
|
||||
config.featureToggles.favoriteDatasources = true;
|
||||
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
||||
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
const favoriteButton = screen.getByTestId('favorite-button');
|
||||
fireEvent.click(favoriteButton);
|
||||
|
||||
expect(mockFavoriteHook.addFavoriteDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(mockFavoriteHook.addFavoriteDatasource).toHaveBeenCalledWith(mockDataSourceInstance);
|
||||
expect(mockFavoriteHook.removeFavoriteDatasource).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove datasource from favorites when favorite button is clicked', () => {
|
||||
config.featureToggles.favoriteDatasources = true;
|
||||
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
||||
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(true);
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
const favoriteButton = screen.getByTestId('favorite-button');
|
||||
fireEvent.click(favoriteButton);
|
||||
|
||||
expect(mockFavoriteHook.removeFavoriteDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(mockFavoriteHook.removeFavoriteDatasource).toHaveBeenCalledWith(mockDataSourceInstance);
|
||||
expect(mockFavoriteHook.addFavoriteDatasource).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call isFavoriteDatasource with correct uid', () => {
|
||||
config.featureToggles.favoriteDatasources = true;
|
||||
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
||||
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
expect(mockFavoriteHook.isFavoriteDatasource).toHaveBeenCalledWith('test-uid');
|
||||
});
|
||||
|
||||
it('should disable favorite button when isLoading is true', () => {
|
||||
config.featureToggles.favoriteDatasources = true;
|
||||
mockUseFavoriteDatasources.mockReturnValue({
|
||||
...mockFavoriteHook,
|
||||
isLoading: true,
|
||||
});
|
||||
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
||||
|
||||
render(<EditDataSourceActions uid="test-uid" />);
|
||||
|
||||
const favoriteButton = screen.getByTestId('favorite-button');
|
||||
expect(favoriteButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { PluginExtensionPoints } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, usePluginLinks } from '@grafana/runtime';
|
||||
import { Button, Dropdown, LinkButton, Menu, Icon } from '@grafana/ui';
|
||||
import { config, usePluginLinks, useFavoriteDatasources, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { Button, Dropdown, LinkButton, Menu, Icon, IconButton } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
||||
import { ALLOWED_DATASOURCE_EXTENSION_PLUGINS } from '../constants';
|
||||
|
|
@ -13,6 +13,36 @@ interface Props {
|
|||
uid: string;
|
||||
}
|
||||
|
||||
const FavoriteButton = ({ uid }: { uid: string }) => {
|
||||
const favoriteDataSources = useFavoriteDatasources();
|
||||
const dataSourceInstance = getDataSourceSrv().getInstanceSettings(uid);
|
||||
const isFavorite = dataSourceInstance ? favoriteDataSources.isFavoriteDatasource(dataSourceInstance.uid) : false;
|
||||
|
||||
return (
|
||||
favoriteDataSources.enabled &&
|
||||
dataSourceInstance &&
|
||||
!dataSourceInstance.meta.builtIn && (
|
||||
<IconButton
|
||||
key={`favorite-${isFavorite ? 'favorite-mono' : 'star-default'}`}
|
||||
name={isFavorite ? 'favorite' : 'star'}
|
||||
iconType={isFavorite ? 'mono' : 'default'}
|
||||
onClick={() =>
|
||||
isFavorite
|
||||
? favoriteDataSources.removeFavoriteDatasource(dataSourceInstance)
|
||||
: favoriteDataSources.addFavoriteDatasource(dataSourceInstance)
|
||||
}
|
||||
disabled={favoriteDataSources.isLoading}
|
||||
tooltip={
|
||||
isFavorite
|
||||
? t('datasources.edit-data-source-actions.remove-favorite', 'Remove from favorites')
|
||||
: t('datasources.edit-data-source-actions.add-favorite', 'Add to favorites')
|
||||
}
|
||||
data-testid="favorite-button"
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export function EditDataSourceActions({ uid }: Props) {
|
||||
const dataSource = useDataSource(uid);
|
||||
const hasExploreRights = contextSrv.hasAccessToExplore();
|
||||
|
|
@ -62,6 +92,7 @@ export function EditDataSourceActions({ uid }: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<FavoriteButton uid={uid} />
|
||||
{hasExploreRights && (
|
||||
<>
|
||||
{!hasActions ? (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { css, cx } from '@emotion/css';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { config, getTemplateSrv, useFavoriteDatasources } from '@grafana/runtime';
|
||||
import { getTemplateSrv, useFavoriteDatasources } from '@grafana/runtime';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { useDatasources, useKeyboardNavigatableList, useRecentlyUsedDataSources } from '../../hooks';
|
||||
|
|
@ -58,9 +58,8 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||
const styles = getStyles(theme, selectedItemCssSelector);
|
||||
|
||||
const { className, current, onChange, enableKeyboardNavigation, onClickEmptyStateCTA } = props;
|
||||
const dataSources =
|
||||
props.dataSources ||
|
||||
useDatasources({
|
||||
const dataSources = useDatasources(
|
||||
{
|
||||
alerting: props.alerting,
|
||||
annotations: props.annotations,
|
||||
dashboard: props.dashboard,
|
||||
|
|
@ -71,29 +70,12 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||
tracing: props.tracing,
|
||||
type: props.type,
|
||||
variables: props.variables,
|
||||
});
|
||||
},
|
||||
props.dataSources
|
||||
);
|
||||
|
||||
const [recentlyUsedDataSources, pushRecentlyUsedDataSource] = useRecentlyUsedDataSources();
|
||||
|
||||
const favoriteDataSourcesHook = config.featureToggles.favoriteDatasources ? useFavoriteDatasources() : null;
|
||||
const storedFavoriteDataSources = favoriteDataSourcesHook?.initialFavoriteDataSources;
|
||||
const isFavoriteDatasource = favoriteDataSourcesHook?.isFavoriteDatasource;
|
||||
|
||||
const toggleFavoriteDatasource = useCallback(
|
||||
(ds: DataSourceInstanceSettings) => {
|
||||
if (!favoriteDataSourcesHook) {
|
||||
return;
|
||||
}
|
||||
const { isFavoriteDatasource, addFavoriteDatasource, removeFavoriteDatasource } = favoriteDataSourcesHook;
|
||||
|
||||
if (isFavoriteDatasource(ds.uid)) {
|
||||
removeFavoriteDatasource(ds);
|
||||
} else {
|
||||
addFavoriteDatasource(ds);
|
||||
}
|
||||
},
|
||||
[favoriteDataSourcesHook]
|
||||
);
|
||||
const favoriteDataSources = useFavoriteDatasources();
|
||||
|
||||
const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources;
|
||||
|
||||
|
|
@ -112,7 +94,7 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||
current,
|
||||
recentlyUsedDataSources,
|
||||
getDataSourceVariableIDs(),
|
||||
storedFavoriteDataSources
|
||||
favoriteDataSources.enabled ? favoriteDataSources.initialFavoriteDataSources : undefined
|
||||
)
|
||||
)
|
||||
.map((ds) => (
|
||||
|
|
@ -125,8 +107,16 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||
onChange(ds);
|
||||
}}
|
||||
selected={isDataSourceMatch(ds, current)}
|
||||
isFavorite={isFavoriteDatasource ? isFavoriteDatasource(ds.uid) : undefined}
|
||||
onToggleFavorite={toggleFavoriteDatasource}
|
||||
isFavorite={favoriteDataSources.isFavoriteDatasource(ds.uid)}
|
||||
onToggleFavorite={
|
||||
favoriteDataSources.enabled
|
||||
? () => {
|
||||
favoriteDataSources.isFavoriteDatasource(ds.uid)
|
||||
? favoriteDataSources.removeFavoriteDatasource(ds)
|
||||
: favoriteDataSources.addFavoriteDatasource(ds);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
{...(enableKeyboardNavigation ? navigatableProps : {})}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ export function useRecentlyUsedDataSources(): [string[], (ds: DataSourceInstance
|
|||
return [value, pushRecentlyUsedDataSource];
|
||||
}
|
||||
|
||||
export function useDatasources(filters: GetDataSourceListFilters) {
|
||||
export function useDatasources(filters: GetDataSourceListFilters, datasources?: DataSourceInstanceSettings[]) {
|
||||
if (datasources) {
|
||||
return datasources;
|
||||
}
|
||||
const dataSourceSrv = getDataSourceSrv();
|
||||
const dataSources = dataSourceSrv.getList(filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import memoizeOne from 'memoize-one';
|
|||
import { DataSourcePluginMeta, DataSourceSettings, UrlQueryValue } from '@grafana/data';
|
||||
import { DataSourcesState } from 'app/types/datasources';
|
||||
|
||||
// Use consistent references for empty objects to prevent infinite re-renders
|
||||
const EMPTY_DATASOURCE = {} as DataSourceSettings;
|
||||
const EMPTY_DATASOURCE_META = {} as DataSourcePluginMeta;
|
||||
|
||||
export const getDataSources = memoizeOne((state: DataSourcesState) => {
|
||||
const regex = new RegExp(state.searchQuery, 'i');
|
||||
|
||||
|
|
@ -27,7 +31,7 @@ export const getDataSource = (state: DataSourcesState, dataSourceId: UrlQueryVal
|
|||
if (state.dataSource.uid === dataSourceId) {
|
||||
return state.dataSource;
|
||||
}
|
||||
return {} as DataSourceSettings;
|
||||
return EMPTY_DATASOURCE;
|
||||
};
|
||||
|
||||
export const getDataSourceMeta = (state: DataSourcesState, type: string): DataSourcePluginMeta => {
|
||||
|
|
@ -35,7 +39,7 @@ export const getDataSourceMeta = (state: DataSourcesState, type: string): DataSo
|
|||
return state.dataSourceMeta;
|
||||
}
|
||||
|
||||
return {} as DataSourcePluginMeta;
|
||||
return EMPTY_DATASOURCE_META;
|
||||
};
|
||||
|
||||
export const getDataSourcesSearchQuery = (state: DataSourcesState) => state.searchQuery;
|
||||
|
|
|
|||
|
|
@ -6552,9 +6552,11 @@
|
|||
"explore": "Explore"
|
||||
},
|
||||
"edit-data-source-actions": {
|
||||
"add-favorite": "Add to favorites",
|
||||
"build-a-dashboard": "Build a dashboard",
|
||||
"explore-data": "Explore data",
|
||||
"open-in-explore": "Open in Explore View"
|
||||
"open-in-explore": "Open in Explore View",
|
||||
"remove-favorite": "Remove from favorites"
|
||||
},
|
||||
"error-details-link": {
|
||||
"aria-label-more-details-about-the-error": "More details about the error"
|
||||
|
|
|
|||
Loading…
Reference in New Issue