import { LoadingSpinner } from '@sourcegraph/react-loading-spinner';
import { sortBy } from 'lodash';
import AlertCircleIcon from 'mdi-react/AlertCircleIcon';
import BugIcon from 'mdi-react/BugIcon';
import DownloadIcon from 'mdi-react/DownloadIcon';
import GithubCircleIcon from 'mdi-react/GithubCircleIcon';
import RefreshIcon from 'mdi-react/RefreshIcon';
import WebIcon from 'mdi-react/WebIcon';
import * as React from 'react';
import { concat, merge, Subject, Subscription } from 'rxjs';
import { catchError, concatMap, delay, filter, map, repeatWhen, switchMap, tap } from 'rxjs/operators';
import { eventLogger } from '../tracking/eventLogger';
import { disableLangServer, enableLangServer, fetchLangServers, restartLangServer, updateLangServer } from './backend';
/**
 * Component to show the status of language servers.
 */
export class SiteAdminLangServers extends React.PureComponent {
    constructor() {
        super(...arguments);
        this.state = {
            langServers: [],
            loading: false,
            errorsBylanguage: new Map(),
            pendingStateByLanguage: new Map(),
        };
        this.subscriptions = new Subscription();
        this.refreshLangServers = new Subject();
        this.updateButtonClicks = new Subject();
        this.restartButtonClicks = new Subject();
        this.disableButtonClicks = new Subject();
        this.enableButtonClicks = new Subject();
        this.EXPERIMENTAL_LANGUAGE_SERVER_WARNING = 'This language server is experimental and under active development. ' +
            'Be aware that it may run arbitrary code through the package manager ' +
            'during dependency installation for each repository. Some extensions ' +
            'to the language server protocol such as cross-repository code ' +
            'intelligence might not be available. Are you sure you want to ' +
            'enable it?';
        this.renderActions = (langServer) => {
            // Disable the action buttons while any of these conditions hold:
            //
            // - The state is expected to change (e.g. after clicking one of the
            //   action buttons)
            // - The language server is currently being downloaded
            const disabled = this.state.pendingStateByLanguage.has(langServer.language) || langServer.downloading;
            const updating = this.state.pendingStateByLanguage.has(langServer.language) &&
                this.state.pendingStateByLanguage.get(langServer.language) === 'updating';
            return (React.createElement("div", { className: "site-admin-lang-servers__actions btn-group", role: "group" },
                updating && (React.createElement("span", { className: "site-admin-lang-servers__actions-updating" }, "Pulling latest Docker image\u2026")),
                langServer.canUpdate && (React.createElement("button", { disabled: disabled, type: "button", className: "site-admin-lang-servers__actions-update btn btn-secondary", "data-tooltip": !disabled ? 'Update language server' : undefined, 
                    // tslint:disable-next-line:jsx-no-lambda
                    onClick: () => this.updateButtonClicks.next(langServer) },
                    React.createElement(DownloadIcon, { className: "icon-inline" }))),
                langServer.state !== 'LANG_SERVER_STATE_DISABLED' &&
                    langServer.canRestart && (React.createElement("button", { disabled: disabled, type: "button", className: "site-admin-lang-servers__actions-restart btn btn-secondary", "data-tooltip": !disabled ? 'Restart language server' : undefined, 
                    // tslint:disable-next-line:jsx-no-lambda
                    onClick: () => this.restartButtonClicks.next(langServer) },
                    React.createElement(RefreshIcon, { className: "icon-inline" }))),
                langServer.state === 'LANG_SERVER_STATE_ENABLED' &&
                    langServer.canDisable && (React.createElement("button", { disabled: disabled, type: "button", className: "site-admin-lang-servers__actions-enable-disable btn btn-secondary", 
                    // tslint:disable-next-line:jsx-no-lambda
                    onClick: () => this.disableButtonClicks.next(langServer) }, "Disable")),
                (langServer.state === 'LANG_SERVER_STATE_DISABLED' || langServer.state === 'LANG_SERVER_STATE_NONE') &&
                    langServer.canEnable && (React.createElement("button", { disabled: disabled, type: "button", className: "btn btn-secondary site-admin-lang-servers__actions-enable-disable", 
                    // tslint:disable-next-line:jsx-no-lambda
                    onClick: () => this.enableButtonClicks.next(langServer) }, "Enable"))));
        };
        this.renderRepo = (langServer) => {
            if (!langServer.homepageURL && !langServer.docsURL && !langServer.issuesURL) {
                return null;
            }
            return (React.createElement("small", null,
                React.createElement("div", { className: "site-admin-lang-servers__repo" },
                    langServer.homepageURL &&
                        langServer.homepageURL.startsWith('https://github.com') && (React.createElement(React.Fragment, null,
                        React.createElement("a", { className: "site-admin-lang-servers__repo-link", href: langServer.homepageURL, target: "_blank", onClick: this.generateClickHandler('LangServerHomepageClicked', langServer) },
                            React.createElement(GithubCircleIcon, { className: "icon-inline" }),
                            ' ',
                            langServer.homepageURL.substr('https://github.com/'.length)))),
                    langServer.homepageURL &&
                        !langServer.homepageURL.startsWith('https://github.com') && (React.createElement("a", { className: "site-admin-lang-servers__repo-link", href: langServer.homepageURL, target: "_blank", onClick: this.generateClickHandler('LangServerHomepageClicked', langServer) }, langServer.homepageURL)),
                    langServer.issuesURL && (React.createElement("a", { className: "site-admin-lang-servers__repo-link", href: langServer.issuesURL, target: "_blank", "data-tooltip": "View issues", onClick: this.generateClickHandler('LangServerIssuesClicked', langServer) },
                        React.createElement(BugIcon, { className: "icon-inline" }))))));
        };
    }
    componentDidMount() {
        this.subscriptions.add(merge(this.updateButtonClicks.pipe(tap(langServer => this.logClick('LangServerUpdateClicked', langServer)), map(langServer => ({
            langServer,
            mutation: updateLangServer,
            errorEventLabel: 'LangServersUpdateFailed',
            stateKey: 'updating',
        }))), this.restartButtonClicks.pipe(tap(langServer => this.logClick('LangServerRestartClicked', langServer)), map(langServer => ({
            langServer,
            mutation: restartLangServer,
            errorEventLabel: 'LangServersRestartFailed',
            stateKey: 'restarting',
        }))), this.disableButtonClicks.pipe(tap(langServer => this.logClick('LangServerDisableClicked', langServer)), map(langServer => ({
            langServer,
            mutation: disableLangServer,
            errorEventLabel: 'LangServersDisableFailed',
            stateKey: 'disabling',
        }))), this.enableButtonClicks.pipe(tap(langServer => this.logClick('LangServerEnableClicked', langServer)), filter(langServer => langServer.experimental ? window.confirm(this.EXPERIMENTAL_LANGUAGE_SERVER_WARNING) : true), map(langServer => ({
            langServer,
            mutation: enableLangServer,
            errorEventLabel: 'LangServersEnableFailed',
            stateKey: 'enabling',
        }))))
            .pipe(tap(({ langServer, stateKey }) => {
            this.setState(prevState => {
                const newErrorsByLanguage = new Map(this.state.errorsBylanguage);
                newErrorsByLanguage.delete(langServer.language);
                return {
                    pendingStateByLanguage: new Map(this.state.pendingStateByLanguage).set(langServer.language, stateKey),
                    errorsBylanguage: newErrorsByLanguage,
                };
            });
        }), concatMap(({ langServer, mutation, errorEventLabel }) => mutation(langServer.language).pipe(map(() => ({
            langServer,
            newState: (prevState) => ({}),
        })), catchError(error => {
            eventLogger.log(errorEventLabel, {
                lang_server: { error_message: error.message },
            });
            console.error(error);
            return [
                {
                    langServer,
                    newState: (prevState) => ({
                        errorsBylanguage: new Map(prevState.errorsBylanguage).set(langServer.language, error),
                    }),
                },
            ];
        }))))
            .subscribe(({ langServer, newState }) => {
            this.refreshLangServers.next();
            const newPendingStateByLanguage = this.state.pendingStateByLanguage;
            newPendingStateByLanguage.delete(langServer.language);
            this.setState(prevState => (Object.assign({}, prevState, newState(prevState), { pendingStateByLanguage: newPendingStateByLanguage })));
        }, err => console.error(err)));
        this.subscriptions.add(
        // Trigger poll sequences initially and on every button click
        concat([undefined], this.refreshLangServers)
            .pipe(
        // When we forced a refresh, cancel the previous
        // in-flight request and start a new poll sequence
        switchMap(() => fetchLangServers().pipe(
        // Map to state update
        map(langServers => ({ langServers, error: undefined, loading: false })), catchError(error => {
            eventLogger.log('LangServersFetchFailed', {
                langServers: { error_message: error.message },
            });
            console.error(error);
            return [{ langServers: [], error, loading: false }];
        }), 
        // After the request completed, resubscribe after 5s to trigger another request
        repeatWhen(delay(2500)))))
            .subscribe(newState => {
            this.setState(newState);
        }));
    }
    componentWillUnmount() {
        this.subscriptions.unsubscribe();
    }
    render() {
        return (React.createElement("div", { className: "site-admin-lang-servers mb-3" },
            React.createElement("div", { className: "site-admin-lang-servers__header" },
                React.createElement("div", { className: "site-admin-lang-servers__header-icon" },
                    React.createElement(WebIcon, { className: "icon-inline" })),
                React.createElement("h5", { className: "site-admin-lang-servers__header-title" }, "Language servers")),
            !this.state.error &&
                this.state.langServers.length === 0 && (React.createElement(LoadingSpinner, { className: "site-admin-lang-servers__loading-icon" })),
            this.state.error && (React.createElement("div", { className: "site-admin-lang-servers__error" },
                React.createElement(AlertCircleIcon, { className: "icon-inline" }),
                React.createElement("span", { className: "site-admin-lang-servers__error-text" },
                    "Error: ",
                    this.state.error.message))),
            // experimental ones are at the bottom of the list.
            sortBy(this.state.langServers, langServer => (langServer.experimental ? 1 : 0)).map((langServer, i) => (React.createElement("div", { className: "site-admin-lang-servers__list-item", key: i },
                React.createElement("div", { className: "site-admin-lang-servers__left-area" },
                    React.createElement("div", { className: "site-admin-lang-servers__language" },
                        React.createElement("div", { className: "site-admin-lang-servers__language-name" }, langServer.displayName),
                        langServer.experimental && (React.createElement("span", { className: "badge badge-warning", "data-tooltip": "This language server is experimental. Beware it may run arbitrary code and might have limited functionality." }, "experimental")),
                        langServer.custom && (React.createElement("span", { className: "site-admin-lang-servers__language-custom", "data-tooltip": "This language server is custom / does not come built in with Sourcegraph. It was added via the site configuration." }, "(custom)"))),
                    this.renderRepo(langServer)),
                React.createElement("div", null,
                    this.renderActions(langServer),
                    this.renderStatus(langServer)))))));
    }
    renderStatus(langServer) {
        // If any action is currently pending, then disregard the langserver
        // state we have from the backend and just display the pending
        // indicator.
        if (this.state.pendingStateByLanguage.has(langServer.language)) {
            return (React.createElement("span", { className: "site-admin-lang-servers__status site-admin-lang-servers__status--pending" },
                React.createElement(LoadingSpinner, { className: "icon-inline" })));
        }
        if (langServer.state === 'LANG_SERVER_STATE_NONE') {
            return null;
        }
        if (langServer.state === 'LANG_SERVER_STATE_DISABLED') {
            return (React.createElement("span", { className: "site-admin-lang-servers__status site-admin-lang-servers__status--disabled" }, "\u25CB Disabled"));
        }
        // TODO(chris): Remove this when more robust health checking is
        // available (e.g. TCP checks).
        const couldDetermineHealth = langServer.pending ||
            langServer.canEnable ||
            langServer.canDisable ||
            langServer.canRestart ||
            langServer.canUpdate ||
            langServer.healthy;
        // In cluster deployments (and in dev mode without DEBUG_MANAGE_DOCKER=t) the
        // docker socket is unavailable, which prevents the backend from running
        // `docker inspect` to determine if the language server is healthy.
        // Custom language servers are not managed by our infrastructure, so we
        // have no insight into their state other than the associated TCP
        // connection or stdio handles to the process. This could be improved in
        // the future by checking that the TCP connection is still open or that
        // the process is still running.
        if (langServer.isClusterDeployment || langServer.custom || !couldDetermineHealth) {
            return (React.createElement("span", { className: "site-admin-lang-servers__status site-admin-lang-servers__status--running" }, "\u25CF Enabled"));
        }
        // Code past here uses fields that are not present for clusters
        if (langServer.pending) {
            return (React.createElement("span", { className: "site-admin-lang-servers__status site-admin-lang-servers__status--pending" },
                React.createElement(LoadingSpinner, { className: "icon-inline" })));
        }
        if (langServer.healthy) {
            return (React.createElement("span", { className: "site-admin-lang-servers__status site-admin-lang-servers__status--running" }, "\u25CF Running"));
        }
        return (React.createElement("span", { className: "site-admin-lang-servers__status site-admin-lang-servers__status--unhealthy" }, "\u25CF Unhealthy"));
    }
    logClick(eventLabel, langServer) {
        eventLogger.log(eventLabel, {
            // 🚨 PRIVACY: never provide any private data in { lang_server: { ... } }.
            lang_server: {
                language: langServer.language,
            },
        });
    }
    generateClickHandler(eventLabel, langServer) {
        return () => this.logClick(eventLabel, langServer);
    }
}
//# sourceMappingURL=SiteAdminLangServers.js.map