mirror of
https://github.com/grafana/grafana.git
synced 2024-12-30 10:47:30 -06:00
ShareDrawer: Share link panel (#90549)
This commit is contained in:
parent
90349b21f7
commit
397dfaf679
@ -1,15 +1,15 @@
|
||||
import {
|
||||
getTimeZone,
|
||||
InterpolateFunction,
|
||||
LinkModel,
|
||||
PanelMenuItem,
|
||||
PanelPlugin,
|
||||
PluginExtensionPanelContext,
|
||||
PluginExtensionPoints,
|
||||
getTimeZone,
|
||||
urlUtil,
|
||||
} from '@grafana/data';
|
||||
import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime';
|
||||
import { LocalValueVariable, SceneGridRow, VizPanel, VizPanelMenu, sceneGraph } from '@grafana/scenes';
|
||||
import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes';
|
||||
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { t } from 'app/core/internationalization';
|
||||
@ -21,7 +21,9 @@ import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { SharePanelInternally } from '../sharing/panel-share/SharePanelInternally';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
@ -79,15 +81,42 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
text: t('panel.header-menu.share', `Share`),
|
||||
iconClassName: 'share-alt',
|
||||
onClick: () => {
|
||||
DashboardInteractions.panelMenuItemClicked('share');
|
||||
dashboard.showModal(new ShareModal({ panelRef: panel.getRef() }));
|
||||
},
|
||||
shortcut: 'p s',
|
||||
});
|
||||
if (config.featureToggles.newDashboardSharingComponent) {
|
||||
const subMenu: PanelMenuItem[] = [];
|
||||
subMenu.push({
|
||||
text: t('share-panel.menu.share-link-title', 'Share link'),
|
||||
iconClassName: 'link',
|
||||
shortcut: 'p u',
|
||||
onClick: () => {
|
||||
const drawer = new ShareDrawer({
|
||||
title: t('share-panel.drawer.share-link-title', 'Link settings'),
|
||||
body: new SharePanelInternally({ panelRef: panel.getRef() }),
|
||||
});
|
||||
|
||||
dashboard.showModal(drawer);
|
||||
},
|
||||
});
|
||||
|
||||
items.push({
|
||||
type: 'submenu',
|
||||
text: t('panel.header-menu.share', 'Share'),
|
||||
iconClassName: 'share-alt',
|
||||
subMenu,
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
text: t('panel.header-menu.share', 'Share'),
|
||||
iconClassName: 'share-alt',
|
||||
onClick: () => {
|
||||
DashboardInteractions.panelMenuItemClicked('share');
|
||||
dashboard.showModal(new ShareModal({ panelRef: panel.getRef() }));
|
||||
},
|
||||
shortcut: 'p s',
|
||||
});
|
||||
}
|
||||
|
||||
if (dashboard.state.isEditing && !isRepeat && !isEditingPanel) {
|
||||
moreSubMenu.push({
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { SetPanelAttentionEvent } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { sceneGraph, VizPanel } from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { KeybindingSet } from 'app/core/services/KeybindingSet';
|
||||
|
||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { SharePanelInternally } from '../sharing/panel-share/SharePanelInternally';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
@ -45,12 +48,26 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
|
||||
});
|
||||
|
||||
// Panel share
|
||||
keybindings.addBinding({
|
||||
key: 'p s',
|
||||
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
|
||||
scene.showModal(new ShareModal({ panelRef: vizPanel.getRef() }));
|
||||
}),
|
||||
});
|
||||
if (config.featureToggles.newDashboardSharingComponent) {
|
||||
keybindings.addBinding({
|
||||
key: 'p u',
|
||||
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
|
||||
const drawer = new ShareDrawer({
|
||||
title: t('share-panel.drawer.share-link-title', 'Link settings'),
|
||||
body: new SharePanelInternally({ panelRef: vizPanel.getRef() }),
|
||||
});
|
||||
|
||||
scene.showModal(drawer);
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
keybindings.addBinding({
|
||||
key: 'p s',
|
||||
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
|
||||
scene.showModal(new ShareModal({ panelRef: vizPanel.getRef() }));
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Panel inspect
|
||||
keybindings.addBinding({
|
||||
|
@ -2,12 +2,12 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectRef, VizPanel } from '@grafana/scenes';
|
||||
import { Alert, ClipboardButton, Divider, Label, Spinner, Stack, Switch, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, ClipboardButton, Divider, Text, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
|
||||
|
||||
import ShareInternallyConfiguration from '../../ShareInternallyConfiguration';
|
||||
import { ShareLinkTab } from '../../ShareLinkTab';
|
||||
import { getShareLinkConfiguration } from '../utils';
|
||||
import { getShareLinkConfiguration, updateShareLinkConfiguration } from '../utils';
|
||||
|
||||
export class ShareInternally extends ShareLinkTab {
|
||||
static Component = ShareInternallyRenderer;
|
||||
@ -20,6 +20,39 @@ export class ShareInternally extends ShareLinkTab {
|
||||
useShortUrl,
|
||||
selectedTheme: theme,
|
||||
});
|
||||
|
||||
this.onToggleLockedTime = this.onToggleLockedTime.bind(this);
|
||||
this.onUrlShorten = this.onUrlShorten.bind(this);
|
||||
this.onThemeChange = this.onThemeChange.bind(this);
|
||||
}
|
||||
|
||||
async onToggleLockedTime() {
|
||||
const useLockedTime = !this.state.useLockedTime;
|
||||
updateShareLinkConfiguration({
|
||||
useAbsoluteTimeRange: useLockedTime,
|
||||
useShortUrl: this.state.useShortUrl,
|
||||
theme: this.state.selectedTheme,
|
||||
});
|
||||
await super.onToggleLockedTime();
|
||||
}
|
||||
|
||||
async onUrlShorten() {
|
||||
const useShortUrl = !this.state.useShortUrl;
|
||||
updateShareLinkConfiguration({
|
||||
useShortUrl,
|
||||
useAbsoluteTimeRange: this.state.useLockedTime,
|
||||
theme: this.state.selectedTheme,
|
||||
});
|
||||
await super.onUrlShorten();
|
||||
}
|
||||
|
||||
async onThemeChange(value: string) {
|
||||
updateShareLinkConfiguration({
|
||||
theme: value,
|
||||
useShortUrl: this.state.useShortUrl,
|
||||
useAbsoluteTimeRange: this.state.useLockedTime,
|
||||
});
|
||||
await super.onThemeChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,41 +75,15 @@ function ShareInternallyRenderer({ model }: SceneComponentProps<ShareInternally>
|
||||
</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={2} direction="column">
|
||||
<Stack gap={1} direction="column">
|
||||
<Stack gap={1} alignItems="start">
|
||||
<Switch
|
||||
label={t('link.share.time-range-label', 'Lock time range')}
|
||||
id="share-current-time-range"
|
||||
value={useLockedTime}
|
||||
onChange={model.onToggleLockedTime}
|
||||
/>
|
||||
<Label
|
||||
description={t(
|
||||
'link.share.time-range-description',
|
||||
'Change the current relative time range to an absolute time range'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="link.share.time-range-label">Lock time range</Trans>
|
||||
</Label>
|
||||
</Stack>
|
||||
<Stack gap={1} alignItems="start">
|
||||
<Switch
|
||||
id="share-short-url"
|
||||
value={useShortUrl}
|
||||
label={t('link.share.short-url-label', 'Shorten link')}
|
||||
onChange={model.onUrlShorten}
|
||||
/>
|
||||
<Label>
|
||||
<Trans i18nKey="link.share.short-url-label">Shorten link</Trans>
|
||||
</Label>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ThemePicker selectedTheme={selectedTheme} onChange={model.onThemeChange} />
|
||||
</Stack>
|
||||
{isBuildUrlLoading && <Spinner />}
|
||||
</Stack>
|
||||
<ShareInternallyConfiguration
|
||||
useLockedTime={useLockedTime}
|
||||
onToggleLockedTime={model.onToggleLockedTime}
|
||||
useShortUrl={useShortUrl}
|
||||
onUrlShorten={model.onUrlShorten}
|
||||
selectedTheme={selectedTheme}
|
||||
onChangeTheme={model.onThemeChange}
|
||||
isLoading={isBuildUrlLoading}
|
||||
/>
|
||||
<Divider spacing={1} />
|
||||
<ClipboardButton
|
||||
icon="link"
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { Label, Spinner, Stack, Switch } from '@grafana/ui';
|
||||
|
||||
import { t, Trans } from '../../../core/internationalization';
|
||||
import { ThemePicker } from '../../dashboard/components/ShareModal/ThemePicker';
|
||||
|
||||
interface Props {
|
||||
useLockedTime: boolean;
|
||||
onToggleLockedTime: () => void;
|
||||
useShortUrl: boolean;
|
||||
onUrlShorten: () => void;
|
||||
selectedTheme: string;
|
||||
onChangeTheme: (v: string) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default function ShareInternallyConfiguration({
|
||||
useLockedTime,
|
||||
onToggleLockedTime,
|
||||
useShortUrl,
|
||||
onUrlShorten,
|
||||
onChangeTheme,
|
||||
selectedTheme,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
return (
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={2} direction="column">
|
||||
<Stack gap={1} direction="column">
|
||||
<Stack gap={1} alignItems="start">
|
||||
<Switch
|
||||
label={t('link.share.time-range-label', 'Lock time range')}
|
||||
id="share-current-time-range"
|
||||
value={useLockedTime}
|
||||
onChange={onToggleLockedTime}
|
||||
/>
|
||||
<Label
|
||||
description={t(
|
||||
'link.share.time-range-description',
|
||||
'Change the current relative time range to an absolute time range'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="link.share.time-range-label">Lock time range</Trans>
|
||||
</Label>
|
||||
</Stack>
|
||||
<Stack gap={1} alignItems="start">
|
||||
<Switch
|
||||
id="share-short-url"
|
||||
value={useShortUrl}
|
||||
label={t('link.share.short-url-label', 'Shorten link')}
|
||||
onChange={onUrlShorten}
|
||||
/>
|
||||
<Label>
|
||||
<Trans i18nKey="link.share.short-url-label">Shorten link</Trans>
|
||||
</Label>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ThemePicker selectedTheme={selectedTheme} onChange={onChangeTheme} />
|
||||
</Stack>
|
||||
{isLoading && <Spinner />}
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -13,7 +13,6 @@ import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getDashboardUrl } from '../utils/urlBuilders';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { updateShareLinkConfiguration } from './ShareButton/utils';
|
||||
import { SceneShareTabState } from './types';
|
||||
export interface ShareLinkTabState extends SceneShareTabState, ShareOptions {
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
@ -50,6 +49,10 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
|
||||
this.addActivationHandler(() => {
|
||||
this.buildUrl();
|
||||
});
|
||||
|
||||
this.onToggleLockedTime = this.onToggleLockedTime.bind(this);
|
||||
this.onUrlShorten = this.onUrlShorten.bind(this);
|
||||
this.onThemeChange = this.onThemeChange.bind(this);
|
||||
}
|
||||
|
||||
async buildUrl() {
|
||||
@ -94,37 +97,22 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
|
||||
return t('share-modal.tab-title.link', 'Link');
|
||||
}
|
||||
|
||||
onToggleLockedTime = async () => {
|
||||
async onToggleLockedTime() {
|
||||
const useLockedTime = !this.state.useLockedTime;
|
||||
updateShareLinkConfiguration({
|
||||
useAbsoluteTimeRange: useLockedTime,
|
||||
useShortUrl: this.state.useShortUrl,
|
||||
theme: this.state.selectedTheme,
|
||||
});
|
||||
this.setState({ useLockedTime });
|
||||
await this.buildUrl();
|
||||
};
|
||||
}
|
||||
|
||||
onUrlShorten = async () => {
|
||||
async onUrlShorten() {
|
||||
const useShortUrl = !this.state.useShortUrl;
|
||||
this.setState({ useShortUrl });
|
||||
updateShareLinkConfiguration({
|
||||
useShortUrl,
|
||||
useAbsoluteTimeRange: this.state.useLockedTime,
|
||||
theme: this.state.selectedTheme,
|
||||
});
|
||||
await this.buildUrl();
|
||||
};
|
||||
}
|
||||
|
||||
onThemeChange = async (value: string) => {
|
||||
async onThemeChange(value: string) {
|
||||
this.setState({ selectedTheme: value });
|
||||
updateShareLinkConfiguration({
|
||||
theme: value,
|
||||
useShortUrl: this.state.useShortUrl,
|
||||
useAbsoluteTimeRange: this.state.useLockedTime,
|
||||
});
|
||||
await this.buildUrl();
|
||||
};
|
||||
}
|
||||
|
||||
getShareUrl = () => {
|
||||
return this.state.shareUrl;
|
||||
|
@ -0,0 +1,113 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneComponentProps, SceneObjectRef, VizPanel } from '@grafana/scenes';
|
||||
import { Alert, ClipboardButton, Divider, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import ShareInternallyConfiguration from '../ShareInternallyConfiguration';
|
||||
import { ShareLinkTab } from '../ShareLinkTab';
|
||||
|
||||
export class SharePanelInternally extends ShareLinkTab {
|
||||
static Component = SharePanelInternallyRenderer;
|
||||
|
||||
constructor({ panelRef }: { panelRef?: SceneObjectRef<VizPanel> }) {
|
||||
super({
|
||||
panelRef,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function SharePanelInternallyRenderer({ model }: SceneComponentProps<SharePanelInternally>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { useLockedTime, useShortUrl, selectedTheme, isBuildUrlLoading, imageUrl } = model.useState();
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const isDashboardSaved = Boolean(dashboard.state.uid);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.configDescription}>
|
||||
<Text variant="body">
|
||||
<Trans i18nKey="link.share-panel.config-description">
|
||||
Create a personalized, direct link to share your panel within your organization, with the following
|
||||
customization settings:
|
||||
</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<ShareInternallyConfiguration
|
||||
useLockedTime={useLockedTime}
|
||||
onToggleLockedTime={() => model.onToggleLockedTime()}
|
||||
useShortUrl={useShortUrl}
|
||||
onUrlShorten={() => model.onUrlShorten()}
|
||||
selectedTheme={selectedTheme}
|
||||
onChangeTheme={(t) => model.onThemeChange(t)}
|
||||
isLoading={isBuildUrlLoading}
|
||||
/>
|
||||
<Divider spacing={1} />
|
||||
<Stack gap={2} direction="column">
|
||||
<div className={styles.buttonsContainer}>
|
||||
<Stack gap={1} flex={1} direction={{ xs: 'column', sm: 'row' }}>
|
||||
<ClipboardButton
|
||||
icon="link"
|
||||
variant="primary"
|
||||
fill="outline"
|
||||
disabled={isBuildUrlLoading}
|
||||
getText={model.getShareUrl}
|
||||
onClipboardCopy={model.onCopy}
|
||||
>
|
||||
<Trans i18nKey="link.share.copy-link-button">Copy link</Trans>
|
||||
</ClipboardButton>
|
||||
<LinkButton
|
||||
href={imageUrl}
|
||||
icon="external-link-alt"
|
||||
target="_blank"
|
||||
variant="secondary"
|
||||
fill="solid"
|
||||
disabled={!config.rendererAvailable || !isDashboardSaved}
|
||||
>
|
||||
<Trans i18nKey="link.share-panel.render-image">Render image</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</div>
|
||||
{!isDashboardSaved && (
|
||||
<Alert severity="info" title={t('share-modal.link.save-alert', 'Dashboard is not saved')} bottomSpacing={0}>
|
||||
<Trans i18nKey="share-modal.link.save-dashboard">
|
||||
To render a panel image, you must save the dashboard first.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
{!config.rendererAvailable && (
|
||||
<Alert
|
||||
severity="info"
|
||||
title={t('share-modal.link.render-alert', 'Image renderer plugin not installed')}
|
||||
bottomSpacing={0}
|
||||
>
|
||||
<Trans i18nKey="share-modal.link.render-instructions">
|
||||
To render a panel image, you must install the{' '}
|
||||
<a
|
||||
href="https://grafana.com/grafana/plugins/grafana-image-renderer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="external-link"
|
||||
>
|
||||
Grafana image renderer plugin
|
||||
</a>
|
||||
. Please contact your Grafana administrator to install the plugin.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
configDescription: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
buttonsContainer: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
@ -953,6 +953,10 @@
|
||||
"short-url-label": "Shorten link",
|
||||
"time-range-description": "Change the current relative time range to an absolute time range",
|
||||
"time-range-label": "Lock time range"
|
||||
},
|
||||
"share-panel": {
|
||||
"config-description": "Create a personalized, direct link to share your panel within your organization, with the following customization settings:",
|
||||
"render-image": "Render image"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@ -2009,6 +2013,14 @@
|
||||
"copy-button": "Copy to Clipboard"
|
||||
}
|
||||
},
|
||||
"share-panel": {
|
||||
"drawer": {
|
||||
"share-link-title": "Link settings"
|
||||
},
|
||||
"menu": {
|
||||
"share-link-title": "Share link"
|
||||
}
|
||||
},
|
||||
"share-playlist": {
|
||||
"checkbox-description": "Panel heights will be adjusted to fit screen size",
|
||||
"checkbox-label": "Autofit",
|
||||
|
@ -953,6 +953,10 @@
|
||||
"short-url-label": "Ŝĥőřŧęʼn ľįʼnĸ",
|
||||
"time-range-description": "Cĥäʼnģę ŧĥę čūřřęʼnŧ řęľäŧįvę ŧįmę řäʼnģę ŧő äʼn äþşőľūŧę ŧįmę řäʼnģę",
|
||||
"time-range-label": "Ŀőčĸ ŧįmę řäʼnģę"
|
||||
},
|
||||
"share-panel": {
|
||||
"config-description": "Cřęäŧę ä pęřşőʼnäľįžęđ, đįřęčŧ ľįʼnĸ ŧő şĥäřę yőūř päʼnęľ ŵįŧĥįʼn yőūř őřģäʼnįžäŧįőʼn, ŵįŧĥ ŧĥę ƒőľľőŵįʼnģ čūşŧőmįžäŧįőʼn şęŧŧįʼnģş:",
|
||||
"render-image": "Ŗęʼnđęř įmäģę"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@ -2009,6 +2013,14 @@
|
||||
"copy-button": "Cőpy ŧő Cľįpþőäřđ"
|
||||
}
|
||||
},
|
||||
"share-panel": {
|
||||
"drawer": {
|
||||
"share-link-title": "Ŀįʼnĸ şęŧŧįʼnģş"
|
||||
},
|
||||
"menu": {
|
||||
"share-link-title": "Ŝĥäřę ľįʼnĸ"
|
||||
}
|
||||
},
|
||||
"share-playlist": {
|
||||
"checkbox-description": "Päʼnęľ ĥęįģĥŧş ŵįľľ þę äđĵūşŧęđ ŧő ƒįŧ şčřęęʼn şįžę",
|
||||
"checkbox-label": "Åūŧőƒįŧ",
|
||||
|
Loading…
Reference in New Issue
Block a user