mirror of
https://github.com/grafana/grafana.git
synced 2025-09-27 15:54:07 +08:00
Make Explore plugin exports explicit
This commit is contained in:
@ -2,7 +2,6 @@ import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explor
|
|||||||
import { ExploreState } from 'app/types/explore';
|
import { ExploreState } from 'app/types/explore';
|
||||||
|
|
||||||
const DEFAULT_EXPLORE_STATE: ExploreState = {
|
const DEFAULT_EXPLORE_STATE: ExploreState = {
|
||||||
customComponents: {},
|
|
||||||
datasource: null,
|
datasource: null,
|
||||||
datasourceError: null,
|
datasourceError: null,
|
||||||
datasourceLoading: null,
|
datasourceLoading: null,
|
||||||
|
@ -17,13 +17,14 @@ import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'
|
|||||||
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
||||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
|
|
||||||
import DefaultQueryRows from './QueryRows';
|
import QueryRows from './QueryRows';
|
||||||
import DefaultGraph from './Graph';
|
import Graph from './Graph';
|
||||||
import DefaultLogs from './Logs';
|
import Logs from './Logs';
|
||||||
import DefaultTable from './Table';
|
import Table from './Table';
|
||||||
import ErrorBoundary from './ErrorBoundary';
|
import ErrorBoundary from './ErrorBoundary';
|
||||||
import TimePicker from './TimePicker';
|
import TimePicker from './TimePicker';
|
||||||
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||||
|
import { DataSource } from 'app/types/datasources';
|
||||||
|
|
||||||
const MAX_HISTORY_ITEMS = 100;
|
const MAX_HISTORY_ITEMS = 100;
|
||||||
|
|
||||||
@ -96,7 +97,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
initialQueries = ensureQueries(queries);
|
initialQueries = ensureQueries(queries);
|
||||||
const initialRange = range || { ...DEFAULT_RANGE };
|
const initialRange = range || { ...DEFAULT_RANGE };
|
||||||
this.state = {
|
this.state = {
|
||||||
customComponents: {},
|
|
||||||
datasource: null,
|
datasource: null,
|
||||||
datasourceError: null,
|
datasourceError: null,
|
||||||
datasourceLoading: null,
|
datasourceLoading: null,
|
||||||
@ -149,7 +149,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDatasource(datasource) {
|
async setDatasource(datasource: DataSource) {
|
||||||
const supportsGraph = datasource.meta.metrics;
|
const supportsGraph = datasource.meta.metrics;
|
||||||
const supportsLogs = datasource.meta.logs;
|
const supportsLogs = datasource.meta.logs;
|
||||||
const supportsTable = datasource.meta.metrics;
|
const supportsTable = datasource.meta.metrics;
|
||||||
@ -177,13 +177,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
query: this.queryExpressions[i],
|
query: this.queryExpressions[i],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const customComponents = {
|
// Custom components
|
||||||
...datasource.exploreComponents,
|
const StartPage = datasource.pluginExports.ExploreStartPage;
|
||||||
};
|
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
customComponents,
|
StartPage,
|
||||||
datasource,
|
datasource,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
history,
|
history,
|
||||||
@ -398,9 +397,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
q.query = this.queryExpressions[i];
|
q.query = this.queryExpressions[i];
|
||||||
return i === index
|
return i === index
|
||||||
? {
|
? {
|
||||||
key: generateQueryKey(index),
|
key: generateQueryKey(index),
|
||||||
query: datasource.modifyQuery(q.query, action),
|
query: datasource.modifyQuery(q.query, action),
|
||||||
}
|
}
|
||||||
: q;
|
: q;
|
||||||
});
|
});
|
||||||
nextQueryTransactions = queryTransactions
|
nextQueryTransactions = queryTransactions
|
||||||
@ -734,7 +733,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
render() {
|
render() {
|
||||||
const { position, split } = this.props;
|
const { position, split } = this.props;
|
||||||
const {
|
const {
|
||||||
customComponents,
|
StartPage,
|
||||||
datasource,
|
datasource,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
datasourceLoading,
|
datasourceLoading,
|
||||||
@ -774,14 +773,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
|
queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
|
||||||
);
|
);
|
||||||
const loading = queryTransactions.some(qt => !qt.done);
|
const loading = queryTransactions.some(qt => !qt.done);
|
||||||
const showStartPages = queryTransactions.length === 0 && customComponents.StartPage;
|
const showStartPages = StartPage && queryTransactions.length === 0;
|
||||||
|
|
||||||
// Custom components
|
|
||||||
const Graph = customComponents.Graph || DefaultGraph;
|
|
||||||
const Logs = customComponents.Logs || DefaultLogs;
|
|
||||||
const QueryRows = customComponents.QueryRows || DefaultQueryRows;
|
|
||||||
const StartPage = customComponents.StartPage;
|
|
||||||
const Table = customComponents.Table || DefaultTable;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={exploreClass} ref={this.getRef}>
|
<div className={exploreClass} ref={this.getRef}>
|
||||||
@ -794,12 +786,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="navbar-buttons explore-first-button">
|
<div className="navbar-buttons explore-first-button">
|
||||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||||
Close Split
|
Close Split
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!datasourceMissing ? (
|
{!datasourceMissing ? (
|
||||||
<div className="navbar-buttons">
|
<div className="navbar-buttons">
|
||||||
<Select
|
<Select
|
||||||
@ -858,7 +850,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
{datasource && !datasourceError ? (
|
{datasource && !datasourceError ? (
|
||||||
<div className="explore-container">
|
<div className="explore-container">
|
||||||
<QueryRows
|
<QueryRows
|
||||||
customComponents={customComponents}
|
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
history={history}
|
history={history}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
@ -879,17 +870,17 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
{supportsGraph ? (
|
{supportsGraph ? (
|
||||||
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||||
Graph
|
Graph
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{supportsTable ? (
|
{supportsTable ? (
|
||||||
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
||||||
Table
|
Table
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{supportsLogs ? (
|
{supportsLogs ? (
|
||||||
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
||||||
Logs
|
Logs
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
|
|||||||
return suggestions && suggestions.length > 0;
|
return suggestions && suggestions.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TypeaheadFieldProps {
|
interface QueryFieldProps {
|
||||||
additionalPlugins?: any[];
|
additionalPlugins?: any[];
|
||||||
cleanText?: (text: string) => string;
|
cleanText?: (text: string) => string;
|
||||||
initialValue: string | null;
|
initialValue: string | null;
|
||||||
@ -35,14 +35,14 @@ interface TypeaheadFieldProps {
|
|||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
|
onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
|
||||||
onValueChanged?: (value: Value) => void;
|
onValueChanged?: (value: Value) => void;
|
||||||
onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string;
|
onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
portalOrigin?: string;
|
portalOrigin?: string;
|
||||||
syntax?: string;
|
syntax?: string;
|
||||||
syntaxLoaded?: boolean;
|
syntaxLoaded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TypeaheadFieldState {
|
export interface QueryFieldState {
|
||||||
suggestions: CompletionItemGroup[];
|
suggestions: CompletionItemGroup[];
|
||||||
typeaheadContext: string | null;
|
typeaheadContext: string | null;
|
||||||
typeaheadIndex: number;
|
typeaheadIndex: number;
|
||||||
@ -60,7 +60,7 @@ export interface TypeaheadInput {
|
|||||||
wrapperNode: Element;
|
wrapperNode: Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
|
export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
|
||||||
menuEl: HTMLElement | null;
|
menuEl: HTMLElement | null;
|
||||||
placeholdersBuffer: PlaceholdersBuffer;
|
placeholdersBuffer: PlaceholdersBuffer;
|
||||||
plugins: any[];
|
plugins: any[];
|
||||||
@ -102,7 +102,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: TypeaheadFieldProps) {
|
componentWillReceiveProps(nextProps: QueryFieldProps) {
|
||||||
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
|
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
|
||||||
// Need a bogus edit to re-render the editor after syntax has fully loaded
|
// Need a bogus edit to re-render the editor after syntax has fully loaded
|
||||||
const change = this.state.value
|
const change = this.state.value
|
||||||
|
@ -4,6 +4,7 @@ import { QueryTransaction, HistoryItem, Query, QueryHint } from 'app/types/explo
|
|||||||
|
|
||||||
import DefaultQueryField from './QueryField';
|
import DefaultQueryField from './QueryField';
|
||||||
import QueryTransactionStatus from './QueryTransactionStatus';
|
import QueryTransactionStatus from './QueryTransactionStatus';
|
||||||
|
import { DataSource } from 'app/types';
|
||||||
|
|
||||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
||||||
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||||
@ -23,8 +24,7 @@ interface QueryRowEventHandlers {
|
|||||||
|
|
||||||
interface QueryRowCommonProps {
|
interface QueryRowCommonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
customComponents: any;
|
datasource: DataSource;
|
||||||
datasource: any;
|
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
// Temporarily
|
// Temporarily
|
||||||
supportsLogs?: boolean;
|
supportsLogs?: boolean;
|
||||||
@ -37,7 +37,7 @@ type QueryRowProps = QueryRowCommonProps &
|
|||||||
query: string;
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DefaultQueryRow extends PureComponent<QueryRowProps> {
|
class QueryRow extends PureComponent<QueryRowProps> {
|
||||||
onChangeQuery = (value, override?: boolean) => {
|
onChangeQuery = (value, override?: boolean) => {
|
||||||
const { index, onChangeQuery } = this.props;
|
const { index, onChangeQuery } = this.props;
|
||||||
if (onChangeQuery) {
|
if (onChangeQuery) {
|
||||||
@ -78,11 +78,11 @@ class DefaultQueryRow extends PureComponent<QueryRowProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { customComponents, datasource, history, query, supportsLogs, transactions } = this.props;
|
const { datasource, history, query, supportsLogs, transactions } = this.props;
|
||||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||||
const hint = getFirstHintFromTransactions(transactions);
|
const hint = getFirstHintFromTransactions(transactions);
|
||||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||||
const QueryField = customComponents.QueryField || DefaultQueryField;
|
const QueryField = datasource.pluginExports.ExploreQueryField || DefaultQueryField;
|
||||||
return (
|
return (
|
||||||
<div className="query-row">
|
<div className="query-row">
|
||||||
<div className="query-row-status">
|
<div className="query-row-status">
|
||||||
@ -124,14 +124,12 @@ type QueryRowsProps = QueryRowCommonProps &
|
|||||||
|
|
||||||
export default class QueryRows extends PureComponent<QueryRowsProps> {
|
export default class QueryRows extends PureComponent<QueryRowsProps> {
|
||||||
render() {
|
render() {
|
||||||
const { className = '', customComponents, queries, transactions, ...handlers } = this.props;
|
const { className = '', queries, transactions, ...handlers } = this.props;
|
||||||
const QueryRow = customComponents.QueryRow || DefaultQueryRow;
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{queries.map((q, index) => (
|
{queries.map((q, index) => (
|
||||||
<QueryRow
|
<QueryRow
|
||||||
key={q.key}
|
key={q.key}
|
||||||
customComponents={customComponents}
|
|
||||||
index={index}
|
index={index}
|
||||||
query={q.query}
|
query={q.query}
|
||||||
transactions={transactions.filter(t => t.rowIndex === index)}
|
transactions={transactions.filter(t => t.rowIndex === index)}
|
||||||
|
@ -8,9 +8,10 @@ import { importPluginModule } from './plugin_loader';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataSourceApi } from 'app/types/series';
|
import { DataSourceApi } from 'app/types/series';
|
||||||
|
import { DataSource } from 'app/types';
|
||||||
|
|
||||||
export class DatasourceSrv {
|
export class DatasourceSrv {
|
||||||
datasources: any;
|
datasources: { [name: string]: DataSource };
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $q, private $injector, private $rootScope, private templateSrv) {
|
constructor(private $q, private $injector, private $rootScope, private templateSrv) {
|
||||||
@ -61,10 +62,10 @@ export class DatasourceSrv {
|
|||||||
throw new Error('Plugin module is missing Datasource constructor');
|
throw new Error('Plugin module is missing Datasource constructor');
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
|
const instance: DataSource = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
|
||||||
instance.meta = pluginDef;
|
instance.meta = pluginDef;
|
||||||
instance.name = name;
|
instance.name = name;
|
||||||
instance.exploreComponents = plugin.ExploreComponents;
|
instance.pluginExports = plugin;
|
||||||
this.datasources[name] = instance;
|
this.datasources[name] = instance;
|
||||||
deferred.resolve(instance);
|
deferred.resolve(instance);
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,7 @@ import { TypeaheadOutput } from 'app/types/explore';
|
|||||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||||
import TypeaheadField, { TypeaheadInput, TypeaheadFieldState } from 'app/features/explore/QueryField';
|
import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||||
|
|
||||||
const HISTOGRAM_GROUP = '__histograms__';
|
const HISTOGRAM_GROUP = '__histograms__';
|
||||||
const METRIC_MARK = 'metric';
|
const METRIC_MARK = 'metric';
|
||||||
@ -50,10 +50,7 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
|
|||||||
return [...options, ...metricsOptions];
|
return [...options, ...metricsOptions];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function willApplySuggestion(
|
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
|
||||||
suggestion: string,
|
|
||||||
{ typeaheadContext, typeaheadText }: TypeaheadFieldState
|
|
||||||
): string {
|
|
||||||
// Modify suggestion based on context
|
// Modify suggestion based on context
|
||||||
switch (typeaheadContext) {
|
switch (typeaheadContext) {
|
||||||
case 'context-labels': {
|
case 'context-labels': {
|
||||||
|
@ -9,15 +9,11 @@ class PrometheusAnnotationsQueryCtrl {
|
|||||||
static templateUrl = 'partials/annotations.editor.html';
|
static templateUrl = 'partials/annotations.editor.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExploreComponents = {
|
|
||||||
QueryField: PromQueryField,
|
|
||||||
StartPage: PrometheusStartPage,
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
PrometheusDatasource as Datasource,
|
PrometheusDatasource as Datasource,
|
||||||
PrometheusQueryCtrl as QueryCtrl,
|
PrometheusQueryCtrl as QueryCtrl,
|
||||||
PrometheusConfigCtrl as ConfigCtrl,
|
PrometheusConfigCtrl as ConfigCtrl,
|
||||||
PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||||
ExploreComponents,
|
PromQueryField as ExploreQueryField,
|
||||||
|
PrometheusStartPage as ExploreStartPage,
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
|
import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
|
||||||
import { Plugin } from './plugins';
|
import { Plugin, PluginExports, PluginMeta } from './plugins';
|
||||||
|
|
||||||
export interface DataSource {
|
export interface DataSource {
|
||||||
id: number;
|
id: number;
|
||||||
@ -16,6 +16,10 @@ export interface DataSource {
|
|||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
jsonData: { authType: string; defaultRegion: string };
|
jsonData: { authType: string; defaultRegion: string };
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
|
meta?: PluginMeta;
|
||||||
|
pluginExports?: PluginExports;
|
||||||
|
init?: () => void;
|
||||||
|
testDatasource?: () => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataSourcesState {
|
export interface DataSourcesState {
|
||||||
|
@ -146,7 +146,7 @@ export interface TextMatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreState {
|
export interface ExploreState {
|
||||||
customComponents: any;
|
StartPage?: any;
|
||||||
datasource: any;
|
datasource: any;
|
||||||
datasourceError: any;
|
datasourceError: any;
|
||||||
datasourceLoading: boolean | null;
|
datasourceLoading: boolean | null;
|
||||||
|
@ -6,7 +6,8 @@ export interface PluginExports {
|
|||||||
ConfigCtrl?: any;
|
ConfigCtrl?: any;
|
||||||
AnnotationsQueryCtrl?: any;
|
AnnotationsQueryCtrl?: any;
|
||||||
PanelOptions?: any;
|
PanelOptions?: any;
|
||||||
ExploreComponents?: any;
|
ExploreQueryField?: any;
|
||||||
|
ExploreStartPage?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelPlugin {
|
export interface PanelPlugin {
|
||||||
@ -26,6 +27,12 @@ export interface PluginMeta {
|
|||||||
name: string;
|
name: string;
|
||||||
info: PluginMetaInfo;
|
info: PluginMetaInfo;
|
||||||
includes: PluginInclude[];
|
includes: PluginInclude[];
|
||||||
|
|
||||||
|
// Datasource-specific
|
||||||
|
metrics?: boolean;
|
||||||
|
logs?: boolean;
|
||||||
|
explore?: boolean;
|
||||||
|
annotations?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginInclude {
|
export interface PluginInclude {
|
||||||
|
Reference in New Issue
Block a user