mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-05 18:47:41 +00:00
Compare commits
2 Commits
languy-com
...
users/vira
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b52f425b4c | ||
|
|
3530633fa2 |
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"GITHUB_CLIENT_ID": "167ea4b09801db1de03d",
|
|
||||||
"GITHUB_CLIENT_SECRET": "e7bb10a3a8da428815805c6fc483560a035a73c1"
|
|
||||||
}
|
|
||||||
@@ -21,17 +21,13 @@ module.exports = {
|
|||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
|
|
||||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
// collectCoverageFrom: [
|
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||||
// "src/Common/Headers*"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// The directory where Jest should output its coverage files
|
// The directory where Jest should output its coverage files
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
|
||||||
// An array of regexp pattern strings used to skip coverage collection
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
// coveragePathIgnorePatterns: [
|
coveragePathIgnorePatterns: ["/node_modules/"],
|
||||||
// "/node_modules/"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// A list of reporter names that Jest uses when writing coverage reports
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
coverageReporters: ["json", "text", "cobertura"],
|
coverageReporters: ["json", "text", "cobertura"],
|
||||||
@@ -39,10 +35,10 @@ module.exports = {
|
|||||||
// An object that configures minimum threshold enforcement for coverage results
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
branches: 22,
|
branches: 25,
|
||||||
functions: 28,
|
functions: 25,
|
||||||
lines: 33,
|
lines: 30,
|
||||||
statements: 31,
|
statements: 30,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
26359
package-lock.json
generated
26359
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -349,8 +349,8 @@ export default class Explorer {
|
|||||||
async () => {
|
async () => {
|
||||||
this.isNotebookEnabled(
|
this.isNotebookEnabled(
|
||||||
!this.isAuthWithResourceToken() &&
|
!this.isAuthWithResourceToken() &&
|
||||||
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
|
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
|
||||||
this.isFeatureEnabled(Constants.Features.enableNotebooks))
|
this.isFeatureEnabled(Constants.Features.enableNotebooks))
|
||||||
);
|
);
|
||||||
|
|
||||||
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
||||||
@@ -372,7 +372,7 @@ export default class Explorer {
|
|||||||
this.isSparkEnabledForAccount() &&
|
this.isSparkEnabledForAccount() &&
|
||||||
this.arcadiaWorkspaces() &&
|
this.arcadiaWorkspaces() &&
|
||||||
this.arcadiaWorkspaces().length > 0) ||
|
this.arcadiaWorkspaces().length > 0) ||
|
||||||
this.isFeatureEnabled(Constants.Features.enableSpark)
|
this.isFeatureEnabled(Constants.Features.enableSpark)
|
||||||
);
|
);
|
||||||
if (this.isSparkEnabled()) {
|
if (this.isSparkEnabled()) {
|
||||||
appInsights.trackEvent(
|
appInsights.trackEvent(
|
||||||
@@ -2006,7 +2006,11 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const databaseAccount = this.databaseAccount();
|
const databaseAccount = this.databaseAccount();
|
||||||
const databaseAccountLocation = databaseAccount && databaseAccount.location.toLowerCase();
|
const databaseAccountLocations = databaseAccount && [
|
||||||
|
databaseAccount?.location.toLowerCase(),
|
||||||
|
...databaseAccount?.properties?.readLocations?.map(location => location?.locationName?.toLowerCase()),
|
||||||
|
...databaseAccount?.properties?.writeLocations?.map(location => location?.locationName?.toLowerCase())
|
||||||
|
];
|
||||||
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
try {
|
try {
|
||||||
@@ -2031,9 +2035,15 @@ export default class Explorer {
|
|||||||
this.isNotebooksEnabledForAccount(true);
|
this.isNotebooksEnabledForAccount(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isAccountInAllowedLocation = !disallowedLocations.some(
|
const isAccountInAllowedLocation = databaseAccountLocations?.some(
|
||||||
(disallowedLocation) => disallowedLocation === databaseAccountLocation
|
(accountLocation) => {
|
||||||
);
|
if (!accountLocation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !disallowedLocations.some(disallowedLocation => disallowedLocation === accountLocation); // not a disallowed location
|
||||||
|
}
|
||||||
|
) || false;
|
||||||
this.isNotebooksEnabledForAccount(isAccountInAllowedLocation);
|
this.isNotebooksEnabledForAccount(isAccountInAllowedLocation);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
|
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
|
||||||
@@ -2532,12 +2542,12 @@ export default class Explorer {
|
|||||||
this.isFeatureEnabled(Constants.Features.enableKOPanel)
|
this.isFeatureEnabled(Constants.Features.enableKOPanel)
|
||||||
? this.deleteCollectionConfirmationPane.open()
|
? this.deleteCollectionConfirmationPane.open()
|
||||||
: this.openSidePanel(
|
: this.openSidePanel(
|
||||||
"Delete Collection",
|
"Delete Collection",
|
||||||
<DeleteCollectionConfirmationPanel
|
<DeleteCollectionConfirmationPanel
|
||||||
explorer={this}
|
explorer={this}
|
||||||
closePanel={() => this.closeSidePanel()}
|
closePanel={() => this.closeSidePanel()}
|
||||||
openNotificationConsole={() => this.expandConsole()}
|
openNotificationConsole={() => this.expandConsole()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
/**
|
|
||||||
* This adapter is responsible to render the React component
|
|
||||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
|
||||||
* and update any knockout observables passed from the parent.
|
|
||||||
*/
|
|
||||||
import * as ko from "knockout";
|
|
||||||
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
|
||||||
import * as React from "react";
|
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
|
||||||
|
|
||||||
export interface CommandBarComponentProps {
|
|
||||||
isNotebookTabActive: boolean;
|
|
||||||
tabsButtons: CommandButtonComponentProps[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandBarComponent: React.FunctionComponent = ({ isNotebookTabActive, tabsButtons }: CommandBarComponentProps) {
|
|
||||||
|
|
||||||
constructor(props: CommandBarComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isNotebookTabActive: false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.container = container;
|
|
||||||
this.tabsButtons = [];
|
|
||||||
// this.isNotebookTabActive = ko.computed(() =>
|
|
||||||
// container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
|
|
||||||
const toWatch = [
|
|
||||||
container.isPreferredApiTable,
|
|
||||||
container.isPreferredApiMongoDB,
|
|
||||||
container.isPreferredApiDocumentDB,
|
|
||||||
container.isPreferredApiCassandra,
|
|
||||||
container.isPreferredApiGraph,
|
|
||||||
container.deleteCollectionText,
|
|
||||||
container.deleteDatabaseText,
|
|
||||||
container.addCollectionText,
|
|
||||||
container.addDatabaseText,
|
|
||||||
container.isDatabaseNodeOrNoneSelected,
|
|
||||||
container.isDatabaseNodeSelected,
|
|
||||||
container.isNoneSelected,
|
|
||||||
container.isResourceTokenCollectionNodeSelected,
|
|
||||||
container.isHostedDataExplorerEnabled,
|
|
||||||
container.isSynapseLinkUpdating,
|
|
||||||
container.databaseAccount,
|
|
||||||
this.isNotebookTabActive,
|
|
||||||
container.isServerlessEnabled,
|
|
||||||
];
|
|
||||||
|
|
||||||
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
|
|
||||||
this.parameters = ko.observable(Date.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
|
|
||||||
this.tabsButtons = buttons;
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
|
||||||
|
|
||||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(this.container);
|
|
||||||
const contextButtons = (this.tabsButtons || []).concat(
|
|
||||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(this.container)
|
|
||||||
);
|
|
||||||
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(this.container);
|
|
||||||
|
|
||||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
|
||||||
if (this.tabsButtons && this.tabsButtons.length > 0) {
|
|
||||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
|
||||||
}
|
|
||||||
|
|
||||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
|
||||||
|
|
||||||
if (uiFabricTabsButtons.length > 0) {
|
|
||||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
|
||||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
|
||||||
|
|
||||||
if (props.isNotebookTabActive) {
|
|
||||||
uiFabricControlButtons.unshift(
|
|
||||||
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<div className="commandBarContainer">
|
|
||||||
<CommandBar
|
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
|
||||||
farItems={uiFabricControlButtons}
|
|
||||||
styles={{
|
|
||||||
root: { backgroundColor: backgroundColor },
|
|
||||||
}}
|
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
110
src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
Normal file
110
src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* This adapter is responsible to render the React component
|
||||||
|
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||||
|
* and update any knockout observables passed from the parent.
|
||||||
|
*/
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
|
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||||
|
import { StyleConstants } from "../../../Common/Constants";
|
||||||
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
|
||||||
|
export class CommandBarComponentAdapter implements ReactAdapter {
|
||||||
|
public parameters: ko.Observable<number>;
|
||||||
|
public container: Explorer;
|
||||||
|
private tabsButtons: CommandButtonComponentProps[];
|
||||||
|
private isNotebookTabActive: ko.Computed<boolean>;
|
||||||
|
|
||||||
|
constructor(container: Explorer) {
|
||||||
|
this.container = container;
|
||||||
|
this.tabsButtons = [];
|
||||||
|
this.isNotebookTabActive = ko.computed(() =>
|
||||||
|
container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
|
||||||
|
const toWatch = [
|
||||||
|
container.isPreferredApiTable,
|
||||||
|
container.isPreferredApiMongoDB,
|
||||||
|
container.isPreferredApiDocumentDB,
|
||||||
|
container.isPreferredApiCassandra,
|
||||||
|
container.isPreferredApiGraph,
|
||||||
|
container.deleteCollectionText,
|
||||||
|
container.deleteDatabaseText,
|
||||||
|
container.addCollectionText,
|
||||||
|
container.addDatabaseText,
|
||||||
|
container.isDatabaseNodeOrNoneSelected,
|
||||||
|
container.isDatabaseNodeSelected,
|
||||||
|
container.isNoneSelected,
|
||||||
|
container.isResourceTokenCollectionNodeSelected,
|
||||||
|
container.isHostedDataExplorerEnabled,
|
||||||
|
container.isSynapseLinkUpdating,
|
||||||
|
container.databaseAccount,
|
||||||
|
this.isNotebookTabActive,
|
||||||
|
container.isServerlessEnabled,
|
||||||
|
];
|
||||||
|
|
||||||
|
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
|
||||||
|
this.parameters = ko.observable(Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
|
||||||
|
this.tabsButtons = buttons;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
|
|
||||||
|
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(this.container);
|
||||||
|
const contextButtons = (this.tabsButtons || []).concat(
|
||||||
|
CommandBarComponentButtonFactory.createContextCommandBarButtons(this.container)
|
||||||
|
);
|
||||||
|
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(this.container);
|
||||||
|
|
||||||
|
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||||
|
if (this.tabsButtons && this.tabsButtons.length > 0) {
|
||||||
|
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||||
|
|
||||||
|
if (uiFabricTabsButtons.length > 0) {
|
||||||
|
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||||
|
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
|
if (this.isNotebookTabActive()) {
|
||||||
|
uiFabricControlButtons.unshift(
|
||||||
|
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="commandBarContainer">
|
||||||
|
<CommandBar
|
||||||
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
|
farItems={uiFabricControlButtons}
|
||||||
|
styles={{
|
||||||
|
root: { backgroundColor: backgroundColor },
|
||||||
|
}}
|
||||||
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerRender() {
|
||||||
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,6 @@ import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
|||||||
import { ExplorerParams } from "./Explorer/Explorer";
|
import { ExplorerParams } from "./Explorer/Explorer";
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
|
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
|
||||||
import { CommandBarComponent } from "./Explorer/Menus/CommandBar/CommandBarComponent";
|
|
||||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||||
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
||||||
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||||
@@ -60,7 +59,6 @@ import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
|
|||||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||||
import "./Explorer/Tabs/QueryTab.less";
|
import "./Explorer/Tabs/QueryTab.less";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useExplorerState } from "./hooks/useExplorerState";
|
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
import { useSidePanel } from "./hooks/useSidePanel";
|
import { useSidePanel } from "./hooks/useSidePanel";
|
||||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||||
@@ -101,8 +99,6 @@ const App: React.FunctionComponent = () => {
|
|||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const explorer = useKnockoutExplorer(config?.platform, explorerParams);
|
const explorer = useKnockoutExplorer(config?.platform, explorerParams);
|
||||||
|
|
||||||
const { commandBarProperties } = useExplorerState(explorer);
|
|
||||||
|
|
||||||
if (!explorer) {
|
if (!explorer) {
|
||||||
return <LoadingExplorer />;
|
return <LoadingExplorer />;
|
||||||
}
|
}
|
||||||
@@ -111,7 +107,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
<div className="flexContainer">
|
<div className="flexContainer">
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
|
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
|
||||||
{/* Main Command Bar - Start */}
|
{/* Main Command Bar - Start */}
|
||||||
<CommandBarComponent {...commandBarProperties} />
|
<div data-bind="react: commandBarComponentAdapter" />
|
||||||
{/* Collections Tree and Tabs - Begin */}
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
<div className="resourceTreeAndTabs">
|
<div className="resourceTreeAndTabs">
|
||||||
{/* Collections Tree - Start */}
|
{/* Collections Tree - Start */}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import Explorer from "../Explorer/Explorer";
|
|
||||||
|
|
||||||
export interface ExplorerStateProperties {
|
|
||||||
commandBarProperties: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useExplorerState = (container: Explorer): ExplorerStateProperties => {
|
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user