mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Add install specific version feature in plugins version tab (#93922)
This commit is contained in:
parent
70e05c6a3a
commit
058b28aaad
@ -156,13 +156,19 @@ export async function getProvisionedPlugins(): Promise<ProvisionedPlugin[]> {
|
|||||||
return provisionedPlugins.map((plugin) => ({ slug: plugin.type }));
|
return provisionedPlugins.map((plugin) => ({ slug: plugin.type }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installPlugin(id: string) {
|
export async function installPlugin(id: string, version?: string) {
|
||||||
// This will install the latest compatible version based on the logic
|
// This will install the latest compatible version based on the logic
|
||||||
// on the backend.
|
// on the backend.
|
||||||
return await getBackendSrv().post(`${API_ROOT}/${id}/install`, undefined, {
|
return await getBackendSrv().post(
|
||||||
// Error is displayed in the page
|
`${API_ROOT}/${id}/install`,
|
||||||
showErrorAlert: false,
|
{
|
||||||
});
|
version,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Error is displayed in the page
|
||||||
|
showErrorAlert: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallPlugin(id: string) {
|
export async function uninstallPlugin(id: string) {
|
||||||
|
@ -56,7 +56,11 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
|
|||||||
if (pageId === PluginTabIds.VERSIONS) {
|
if (pageId === PluginTabIds.VERSIONS) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<VersionList versions={plugin.details?.versions} installedVersion={plugin.installedVersion} />
|
<VersionList
|
||||||
|
pluginId={plugin.id}
|
||||||
|
versions={plugin.details?.versions}
|
||||||
|
installedVersion={plugin.installedVersion}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,112 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
|
import { Version } from '../types';
|
||||||
|
|
||||||
|
import { VersionInstallButton } from './VersionInstallButton';
|
||||||
|
|
||||||
|
describe('VersionInstallButton', () => {
|
||||||
|
it('should show install when no version is installed', () => {
|
||||||
|
const version: Version = {
|
||||||
|
version: '',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
};
|
||||||
|
renderWithStore(
|
||||||
|
<VersionInstallButton pluginId={''} version={version} disabled={false} onConfirmInstallation={() => {}} />
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Install')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show upgrade when a lower version is installed', () => {
|
||||||
|
const version: Version = {
|
||||||
|
version: '1.0.1',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
};
|
||||||
|
const installedVersion = '1.0.0';
|
||||||
|
renderWithStore(
|
||||||
|
<VersionInstallButton
|
||||||
|
installedVersion={installedVersion}
|
||||||
|
pluginId={''}
|
||||||
|
version={version}
|
||||||
|
disabled={false}
|
||||||
|
onConfirmInstallation={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Upgrade')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show downgrade when a lower version is installed', () => {
|
||||||
|
const version: Version = {
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
};
|
||||||
|
const installedVersion = '1.0.1';
|
||||||
|
renderWithStore(
|
||||||
|
<VersionInstallButton
|
||||||
|
installedVersion={installedVersion}
|
||||||
|
pluginId={''}
|
||||||
|
version={version}
|
||||||
|
disabled={false}
|
||||||
|
onConfirmInstallation={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Downgrade')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ask for confirmation on downgrade', () => {
|
||||||
|
const version: Version = {
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
};
|
||||||
|
const installedVersion = '1.0.1';
|
||||||
|
renderWithStore(
|
||||||
|
<VersionInstallButton
|
||||||
|
installedVersion={installedVersion}
|
||||||
|
pluginId={''}
|
||||||
|
version={version}
|
||||||
|
disabled={false}
|
||||||
|
onConfirmInstallation={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Downgrade')).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByText('Downgrade'));
|
||||||
|
expect(screen.getByText('Downgrade plugin version')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shown installed text instead of button when version is installed', () => {
|
||||||
|
const version: Version = {
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
};
|
||||||
|
const installedVersion = '1.0.0';
|
||||||
|
renderWithStore(
|
||||||
|
<VersionInstallButton
|
||||||
|
installedVersion={installedVersion}
|
||||||
|
pluginId={''}
|
||||||
|
version={version}
|
||||||
|
disabled={false}
|
||||||
|
onConfirmInstallation={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const el = screen.getByText('Installed');
|
||||||
|
expect(el).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderWithStore(component: JSX.Element) {
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
return render(<Provider store={store}>{component}</Provider>);
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { gt } from 'semver';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
|
import { Badge, Button, ConfirmModal, Icon, Spinner, useStyles2 } from '@grafana/ui';
|
||||||
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
|
import { useInstall } from '../state/hooks';
|
||||||
|
import { Version } from '../types';
|
||||||
|
|
||||||
|
const PLUGINS_VERSION_PAGE_INSTALL_INTERACTION_EVENT_NAME = 'plugins_upgrade_clicked';
|
||||||
|
const PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME = 'plugins_downgrade_clicked';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pluginId: string;
|
||||||
|
version: Version;
|
||||||
|
latestCompatibleVersion?: string;
|
||||||
|
installedVersion?: string;
|
||||||
|
disabled: boolean;
|
||||||
|
onConfirmInstallation: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VersionInstallButton = ({
|
||||||
|
pluginId,
|
||||||
|
version,
|
||||||
|
latestCompatibleVersion,
|
||||||
|
installedVersion,
|
||||||
|
disabled,
|
||||||
|
onConfirmInstallation,
|
||||||
|
}: Props) => {
|
||||||
|
const install = useInstall();
|
||||||
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const isDowngrade = installedVersion && gt(installedVersion, version.version);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (installedVersion === version.version) {
|
||||||
|
setIsInstalling(false);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}
|
||||||
|
}, [installedVersion, version.version]);
|
||||||
|
|
||||||
|
if (version.version === installedVersion) {
|
||||||
|
return <Badge className={styles.badge} text="Installed" icon="check" color="green" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const performInstallation = () => {
|
||||||
|
const trackProps = {
|
||||||
|
path: location.pathname,
|
||||||
|
plugin_id: pluginId,
|
||||||
|
version: version.version,
|
||||||
|
is_latest: latestCompatibleVersion === version.version,
|
||||||
|
creator_team: 'grafana_plugins_catalog',
|
||||||
|
schema_version: '1.0.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!installedVersion) {
|
||||||
|
reportInteraction(PLUGINS_VERSION_PAGE_INSTALL_INTERACTION_EVENT_NAME, trackProps);
|
||||||
|
} else {
|
||||||
|
reportInteraction(PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME, {
|
||||||
|
...trackProps,
|
||||||
|
previous_version: installedVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
install(pluginId, version.version, true);
|
||||||
|
setIsInstalling(true);
|
||||||
|
onConfirmInstallation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInstallClick = () => {
|
||||||
|
if (isDowngrade) {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
} else {
|
||||||
|
performInstallation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
performInstallation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDismiss = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = 'Downgrade';
|
||||||
|
|
||||||
|
if (!installedVersion) {
|
||||||
|
label = 'Install';
|
||||||
|
} else if (gt(version.version, installedVersion)) {
|
||||||
|
label = 'Upgrade';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
fill="solid"
|
||||||
|
disabled={disabled || isInstalling}
|
||||||
|
fullWidth
|
||||||
|
size="sm"
|
||||||
|
variant={latestCompatibleVersion === version.version ? 'primary' : 'secondary'}
|
||||||
|
onClick={onInstallClick}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
{label} {isInstalling ? <Spinner className={styles.spinner} inline size="sm" /> : getIcon(label)}
|
||||||
|
</Button>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
title={t('plugins.catalog.versions.downgrade-title', 'Downgrade plugin version')}
|
||||||
|
body={`${t('plugins.catalog.versions.confirmation-text-1', 'Are you really sure you want to downgrade to version')} ${version.version}? ${t('plugins.catalog.versions.confirmation-text-2', 'You should normally not be doing this')}`}
|
||||||
|
confirmText={t('plugins.catalog.versions.downgrade-confirm', 'Downgrade')}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
disabled={isInstalling}
|
||||||
|
confirmButtonVariant="primary"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIcon(label: string) {
|
||||||
|
if (label === 'Downgrade') {
|
||||||
|
return <Icon name="arrow-down" />;
|
||||||
|
}
|
||||||
|
if (label === 'Upgrade') {
|
||||||
|
return <Icon name="arrow-up" />;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
spinner: css({
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
successIcon: css({
|
||||||
|
color: theme.colors.success.main,
|
||||||
|
}),
|
||||||
|
button: css({
|
||||||
|
width: theme.spacing(13),
|
||||||
|
}),
|
||||||
|
badge: css({
|
||||||
|
width: theme.spacing(13),
|
||||||
|
justifyContent: 'center',
|
||||||
|
}),
|
||||||
|
});
|
@ -0,0 +1,67 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
|
import { VersionList } from './VersionList';
|
||||||
|
|
||||||
|
describe('VersionList', () => {
|
||||||
|
it('should only show installs when no version is installed', () => {
|
||||||
|
const versions = [
|
||||||
|
{
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '1.0.1',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
renderWithStore(<VersionList pluginId={''} versions={versions} />);
|
||||||
|
const installElements = screen.getAllByText('Install');
|
||||||
|
expect(installElements).toHaveLength(versions.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should downgrades and upgrades when one intermediate version is installed', () => {
|
||||||
|
const versions = [
|
||||||
|
{
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '1.0.1',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '1.0.2',
|
||||||
|
createdAt: '',
|
||||||
|
isCompatible: false,
|
||||||
|
grafanaDependency: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const installedVersionIndex = 1;
|
||||||
|
|
||||||
|
renderWithStore(
|
||||||
|
<VersionList pluginId={''} versions={versions} installedVersion={versions[installedVersionIndex].version} />
|
||||||
|
);
|
||||||
|
expect(screen.getAllByText('Installed')).toHaveLength(1);
|
||||||
|
expect(screen.getAllByText('Downgrade')).toHaveLength(1);
|
||||||
|
expect(screen.getAllByText('Upgrade')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderWithStore(component: JSX.Element) {
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
return render(<Provider store={store}>{component}</Provider>);
|
||||||
|
}
|
@ -1,29 +1,48 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { satisfies } from 'semver';
|
||||||
|
|
||||||
import { dateTimeFormatTimeAgo, GrafanaTheme2 } from '@grafana/data';
|
import { dateTimeFormatTimeAgo, GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { getLatestCompatibleVersion } from '../helpers';
|
import { getLatestCompatibleVersion } from '../helpers';
|
||||||
import { Version } from '../types';
|
import { Version } from '../types';
|
||||||
|
|
||||||
|
import { VersionInstallButton } from './VersionInstallButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
pluginId: string;
|
||||||
versions?: Version[];
|
versions?: Version[];
|
||||||
installedVersion?: string;
|
installedVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VersionList = ({ versions = [], installedVersion }: Props) => {
|
export const VersionList = ({ pluginId, versions = [], installedVersion }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const latestCompatibleVersion = getLatestCompatibleVersion(versions);
|
const latestCompatibleVersion = getLatestCompatibleVersion(versions);
|
||||||
|
|
||||||
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
|
|
||||||
|
const grafanaVersion = config.buildInfo.version;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsInstalling(false);
|
||||||
|
}, [installedVersion]);
|
||||||
|
|
||||||
if (versions.length === 0) {
|
if (versions.length === 0) {
|
||||||
return <p>No version history was found.</p>;
|
return <p>No version history was found.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onInstallClick = () => {
|
||||||
|
setIsInstalling(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
|
<th></th>
|
||||||
<th>Last updated</th>
|
<th>Last updated</th>
|
||||||
<th>Grafana Dependency</th>
|
<th>Grafana Dependency</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -31,6 +50,10 @@ export const VersionList = ({ versions = [], installedVersion }: Props) => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{versions.map((version) => {
|
{versions.map((version) => {
|
||||||
const isInstalledVersion = installedVersion === version.version;
|
const isInstalledVersion = installedVersion === version.version;
|
||||||
|
const versionIsIncompatible = version.grafanaDependency
|
||||||
|
? !satisfies(grafanaVersion, version.grafanaDependency, { includePrerelease: true })
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={version.version}>
|
<tr key={version.version}>
|
||||||
{/* Version number */}
|
{/* Version number */}
|
||||||
@ -42,6 +65,18 @@ export const VersionList = ({ versions = [], installedVersion }: Props) => {
|
|||||||
<td>{version.version}</td>
|
<td>{version.version}</td>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Install button */}
|
||||||
|
<td>
|
||||||
|
<VersionInstallButton
|
||||||
|
pluginId={pluginId}
|
||||||
|
version={version}
|
||||||
|
latestCompatibleVersion={latestCompatibleVersion?.version}
|
||||||
|
installedVersion={installedVersion}
|
||||||
|
onConfirmInstallation={onInstallClick}
|
||||||
|
disabled={isInstalledVersion || isInstalling || versionIsIncompatible}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
{/* Last updated */}
|
{/* Last updated */}
|
||||||
<td className={isInstalledVersion ? styles.currentVersion : ''}>
|
<td className={isInstalledVersion ? styles.currentVersion : ''}>
|
||||||
{dateTimeFormatTimeAgo(version.createdAt)}
|
{dateTimeFormatTimeAgo(version.createdAt)}
|
||||||
@ -60,6 +95,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
container: css({
|
container: css({
|
||||||
padding: theme.spacing(2, 4, 3),
|
padding: theme.spacing(2, 4, 3),
|
||||||
}),
|
}),
|
||||||
|
currentVersion: css({
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
}),
|
||||||
|
spinner: css({
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
}),
|
||||||
table: css({
|
table: css({
|
||||||
tableLayout: 'fixed',
|
tableLayout: 'fixed',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -69,8 +110,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
th: {
|
th: {
|
||||||
fontSize: theme.typography.h5.fontSize,
|
fontSize: theme.typography.h5.fontSize,
|
||||||
},
|
},
|
||||||
}),
|
'tbody tr:nth-child(odd)': {
|
||||||
currentVersion: css({
|
background: theme.colors.emphasize(theme.colors.background.primary, 0.02),
|
||||||
fontWeight: theme.typography.fontWeightBold,
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -202,7 +202,7 @@ export const install = createAsyncThunk<
|
|||||||
? { isInstalled: true, installedVersion: version, hasUpdate: false }
|
? { isInstalled: true, installedVersion: version, hasUpdate: false }
|
||||||
: { isInstalled: true, installedVersion: version };
|
: { isInstalled: true, installedVersion: version };
|
||||||
try {
|
try {
|
||||||
await installPlugin(id);
|
await installPlugin(id, version);
|
||||||
await updatePanels();
|
await updatePanels();
|
||||||
|
|
||||||
if (isUpdating) {
|
if (isUpdating) {
|
||||||
|
@ -2176,6 +2176,12 @@
|
|||||||
"name-header": "Name",
|
"name-header": "Name",
|
||||||
"update-header": "Update",
|
"update-header": "Update",
|
||||||
"update-status-text": "plugins updated"
|
"update-status-text": "plugins updated"
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"confirmation-text-1": "Are you really sure you want to downgrade to version",
|
||||||
|
"confirmation-text-2": "You should normally not be doing this",
|
||||||
|
"downgrade-confirm": "Downgrade",
|
||||||
|
"downgrade-title": "Downgrade plugin version"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
|
@ -2176,6 +2176,12 @@
|
|||||||
"name-header": "Ńämę",
|
"name-header": "Ńämę",
|
||||||
"update-header": "Ůpđäŧę",
|
"update-header": "Ůpđäŧę",
|
||||||
"update-status-text": "pľūģįʼnş ūpđäŧęđ"
|
"update-status-text": "pľūģįʼnş ūpđäŧęđ"
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"confirmation-text-1": "Åřę yőū řęäľľy şūřę yőū ŵäʼnŧ ŧő đőŵʼnģřäđę ŧő vęřşįőʼn",
|
||||||
|
"confirmation-text-2": "Ÿőū şĥőūľđ ʼnőřmäľľy ʼnőŧ þę đőįʼnģ ŧĥįş",
|
||||||
|
"downgrade-confirm": "Đőŵʼnģřäđę",
|
||||||
|
"downgrade-title": "Đőŵʼnģřäđę pľūģįʼn vęřşįőʼn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
|
Loading…
Reference in New Issue
Block a user