More Spark UI Cleanup (#89)

Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
This commit is contained in:
Steve Faulkner
2020-07-15 16:59:04 -05:00
committed by GitHub
parent 99c6a7ebcc
commit 444e25c086
21 changed files with 0 additions and 1511 deletions

View File

@@ -1,59 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="clusterLibraryPane">
<!-- Cluster Library -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Cluster Library header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Cluster Library header - End -->
<!-- Cluster Library errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"/></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Cluster Library errors - End -->
<!-- Cluster Library inputs - Start -->
<div class="paneMainContent"><div data-bind="react: clusterLibraryGridAdapter"></div></div>
<!-- Cluster Library inputs - End -->
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Save" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Cluster Library - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,237 +0,0 @@
import _ from "underscore";
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ClusterLibraryGridAdapter } from "../Controls/LibraryManagement/ClusterLibraryGridAdapter";
import { ClusterLibraryGridProps, ClusterLibraryItem } from "../Controls/LibraryManagement/ClusterLibraryGrid";
import { Library, SparkCluster, SparkClusterLibrary } from "../../Contracts/DataModels";
import * as Logger from "../../Common/Logger";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
export class ClusterLibraryPane extends ContextualPaneBase {
public clusterLibraryGridAdapter: ClusterLibraryGridAdapter;
private _clusterLibraryProps: ko.Observable<ClusterLibraryGridProps>;
private _originalCluster: SparkCluster;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Cluster Libraries");
this._clusterLibraryProps = ko.observable<ClusterLibraryGridProps>({
libraryItems: [],
onInstalledChanged: this._onInstalledChanged
});
this.clusterLibraryGridAdapter = new ClusterLibraryGridAdapter();
this.clusterLibraryGridAdapter.parameters = this._clusterLibraryProps;
this.resetData();
}
public open(): void {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
Promise.all([this._getLibraries(resourceId), this._getDefaultCluster(resourceId)]).then(
result => {
const [libraries, cluster] = result;
this._originalCluster = cluster;
const libraryItems = this._mapClusterLibraries(cluster, libraries);
this._updateClusterLibraryGridStates({ libraryItems });
},
reason => {
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
super.open();
}
public submit(): void {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this.isExecuting(true);
if (this._areLibrariesChanged()) {
const newLibraries = this._clusterLibraryProps()
.libraryItems.filter(lib => lib.installed)
.map(lib => ({ name: lib.name }));
this._updateClusterLibraries(resourceId, this._originalCluster, newLibraries).then(
() => {
this.isExecuting(false);
this.close();
},
reason => {
this.isExecuting(false);
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
} else {
this.isExecuting(false);
this.close();
}
}
private _updateClusterLibraryGridStates(states: Partial<ClusterLibraryGridProps>): void {
const merged = { ...this._clusterLibraryProps(), ...states };
this._clusterLibraryProps(merged);
this._clusterLibraryProps.valueHasMutated();
}
private _onInstalledChanged = (libraryName: string, installed: boolean): void => {
const items = this._clusterLibraryProps().libraryItems;
const library = _.find(items, item => item.name === libraryName);
library.installed = installed;
this._clusterLibraryProps.valueHasMutated();
};
private _areLibrariesChanged(): boolean {
const original = this._originalCluster.properties && this._originalCluster.properties.libraries;
const changed = this._clusterLibraryProps()
.libraryItems.filter(lib => lib.installed)
.map(lib => lib.name);
if (original.length !== changed.length) {
return true;
}
const newLibraries = new Set(changed);
for (let o of original) {
if (!newLibraries.has(o.name)) {
return false;
}
newLibraries.delete(o.name);
}
return newLibraries.size === 0;
}
private _mapClusterLibraries(cluster: SparkCluster, libraries: Library[]): ClusterLibraryItem[] {
const clusterLibraries = cluster && cluster.properties && cluster.properties.libraries;
const libraryItems = libraries.map(lib => ({
...lib,
installed: clusterLibraries.some(clusterLib => clusterLib.name === lib.name)
}));
return libraryItems;
}
private async _getLibraries(resourceId: string): Promise<Library[]> {
if (!resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Fetching libraries...`
);
try {
return await this.container.sparkClusterManager.getLibrariesAsync(resourceId);
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch libraries. Reason: ${JSON.stringify(e)}`
);
Logger.logError(e, "Explorer/_getLibraries");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _getDefaultCluster(resourceId: string, clusterId: string = "default"): Promise<SparkCluster> {
if (!resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
const inProgressId = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Fetching cluster...`);
try {
const cluster = await this.container.sparkClusterManager.getClusterAsync(resourceId, clusterId);
return cluster;
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch cluster. Reason: ${JSON.stringify(e)}`
);
Logger.logError(e, "Explorer/_getCluster");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _updateClusterLibraries(
resourceId: string,
originalCluster: SparkCluster,
newLibrarys: SparkClusterLibrary[]
): Promise<void> {
if (!originalCluster || !resourceId) {
return Promise.reject("Invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("Cluster client is not initialized yet");
}
TelemetryProcessor.traceStart(Action.ClusterLibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "ClusterLibraryPane/_updateClusterLibraries",
originalCluster,
newLibrarys
});
let newCluster = originalCluster;
newCluster.properties.libraries = newLibrarys;
const consoleId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating ${newCluster.name} libraries...`
);
try {
const cluster = await this.container.sparkClusterManager.updateClusterAsync(
resourceId,
originalCluster.name,
newCluster
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated ${newCluster.name} libraries.`
);
TelemetryProcessor.traceSuccess(Action.ClusterLibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "ClusterLibraryPane/_updateClusterLibraries",
cluster
});
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to upload ${newCluster.name} libraries. Reason: ${JSON.stringify(e)}`
);
TelemetryProcessor.traceFailure(Action.ClusterLibraryManage, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "ClusterLibraryPane/_updateClusterLibraries",
error: e
});
Logger.logError(e, "Explorer/_updateClusterLibraries");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(consoleId);
}
}
}

View File

@@ -1,55 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="libraryManagePane">
<!-- Library Manage -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Library Manage header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Library Manage header - End -->
<!-- Library Manage errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error"/></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Library Manage errors - End -->
<!-- Library Manage inputs - Start -->
<div class="paneMainContent"><div data-bind="react: libraryManageComponentAdapter"></div></div>
<!-- Library Manage inputs - End -->
</form>
</div>
<!-- Library Manage - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,372 +0,0 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { LibraryManageComponentAdapter } from "../Controls/LibraryManagement/LibraryManageComponentAdapter";
import {
LibraryManageComponentProps,
LibraryAddNameTextFieldProps,
LibraryAddUrlTextFieldProps,
LibraryAddButtonProps,
LibraryManageGridProps
} from "../Controls/LibraryManagement/LibraryManage";
import { Library } from "../../Contracts/DataModels";
import * as Logger from "../../Common/Logger";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
export class LibraryManagePane extends ContextualPaneBase {
public libraryManageComponentAdapter: LibraryManageComponentAdapter;
private _libraryManageProps: ko.Observable<LibraryManageComponentProps>;
private _libraryManageStates: { isNameValid: boolean; isUrlValid: boolean };
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Libraries");
this._libraryManageStates = {
isNameValid: true,
isUrlValid: true
};
this._libraryManageProps = ko.observable<LibraryManageComponentProps>({
addProps: {
nameProps: {
libraryName: "",
onLibraryNameChange: this._onLibraryNameChange,
onLibraryNameValidated: this._onLibraryNameValidated
},
urlProps: {
libraryAddress: "",
onLibraryAddressChange: this._onLibraryAddressChange,
onLibraryAddressValidated: this._onLibraryAddressValidated
},
buttonProps: {
disabled: false,
onLibraryAddClick: this._onLibraryAddClick
}
},
gridProps: {
items: [],
onLibraryDeleteClick: this._onLibraryDeleteClick
}
});
this.libraryManageComponentAdapter = new LibraryManageComponentAdapter();
this.libraryManageComponentAdapter.parameters = this._libraryManageProps;
this.resetData();
}
public open(): void {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this._getLibraries(resourceId).then(
(libraries: Library[]) => {
this._updateLibraryManageComponentProps(null, null, null, {
items: libraries
});
},
reason => {
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
super.open();
}
public submit(): void {
// override default behavior because this is not a form
}
private _updateLibraryManageComponentProps(
newNameProps?: Partial<LibraryAddNameTextFieldProps>,
newUrlProps?: Partial<LibraryAddUrlTextFieldProps>,
newButtonProps?: Partial<LibraryAddButtonProps>,
newGridProps?: Partial<LibraryManageGridProps>
): void {
let {
addProps: { buttonProps, nameProps, urlProps },
gridProps
} = this._libraryManageProps();
if (newNameProps) {
nameProps = { ...nameProps, ...newNameProps };
}
if (newUrlProps) {
urlProps = { ...urlProps, ...newUrlProps };
}
if (newButtonProps) {
buttonProps = { ...buttonProps, ...newButtonProps };
}
if (newGridProps) {
gridProps = { ...gridProps, ...newGridProps };
}
this._libraryManageProps({
addProps: {
nameProps,
urlProps,
buttonProps
},
gridProps
});
this._libraryManageProps.valueHasMutated();
}
private _onLibraryNameChange = (libraryName: string): void => {
this._updateLibraryManageComponentProps({ libraryName });
};
private _onLibraryNameValidated = (errorMessage: string): void => {
this._libraryManageStates.isNameValid = !errorMessage;
this._validateAddButton();
};
private _onLibraryAddressChange = (libraryAddress: string): void => {
this._updateLibraryManageComponentProps(null, {
libraryAddress
});
if (!this._libraryManageProps().addProps.nameProps.libraryName) {
const parsedLibraryAddress = this._parseLibraryUrl(libraryAddress);
if (!parsedLibraryAddress) {
return;
}
let libraryName = this._sanitizeLibraryName(parsedLibraryAddress[2]);
this._updateLibraryManageComponentProps({ libraryName });
}
};
private _sanitizeLibraryName = (libraryName: string): string => {
const invalidCharRegex = /[^a-zA-Z0-9-]/gm;
return libraryName
.replace(invalidCharRegex, "-")
.substring(0, Math.min(Constants.SparkLibrary.nameMaxLength, libraryName.length));
};
private _onLibraryAddressValidated = (errorMessage: string): void => {
this._libraryManageStates.isUrlValid = !errorMessage;
this._validateAddButton();
};
private _validateAddButton = (): void => {
const isValid = this._libraryManageStates.isNameValid && this._libraryManageStates.isUrlValid;
const isUploadDisabled = this._libraryManageProps().addProps.buttonProps.disabled;
if (isValid === isUploadDisabled) {
this._updateLibraryManageComponentProps(null, null, {
disabled: !isUploadDisabled
});
}
};
private _onLibraryDeleteClick = (libraryName: string): void => {
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this.isExecuting(true);
this._deleteLibrary(resourceId, libraryName).then(
() => {
this.isExecuting(false);
const items = this._libraryManageProps().gridProps.items.filter(lib => lib.name !== libraryName);
this._updateLibraryManageComponentProps(null, null, null, {
items
});
},
reason => {
this.isExecuting(false);
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
};
private _onLibraryAddClick = (): void => {
const libraryAddress = this._libraryManageProps().addProps.urlProps.libraryAddress;
if (!libraryAddress) {
this.formErrors("Library Url cannot be null");
return;
}
const libraryName = this._libraryManageProps().addProps.nameProps.libraryName || this._generateLibraryName();
if (!libraryName) {
this.formErrors("Library Name cannot be null");
return;
}
const parsedLibraryAddress = this._parseLibraryUrl(libraryAddress);
if (!parsedLibraryAddress) {
return;
}
const library: Library = {
name: libraryName,
properties: {
kind: "Jar",
source: {
kind: "HttpsUri",
libraryFileName: `${libraryName}.${parsedLibraryAddress[3]}`,
uri: libraryAddress
}
}
};
const resourceId: string = this.container.databaseAccount() && this.container.databaseAccount().id;
this.isExecuting(true);
this._updateLibraryManageComponentProps(null, null, { disabled: true });
this._addLibrary(resourceId, library).then(
() => {
this.isExecuting(false);
this._updateLibraryManageComponentProps(
{
libraryName: ""
},
{
libraryAddress: ""
},
{
disabled: false
},
{
items: [...this._libraryManageProps().gridProps.items, library]
}
);
},
reason => {
this.isExecuting(false);
const parsedError = ErrorParserUtility.parse(reason);
this.formErrors(parsedError[0].message);
}
);
};
private _parseLibraryUrl = (url: string): RegExpExecArray => {
const libraryUrlRegex = /^(https:\/\/.+\/)(.+)\.(jar)$/gi;
return libraryUrlRegex.exec(url);
};
private _generateLibraryName = (): string => {
return `library-${Math.random()
.toString(32)
.substring(2)}`;
};
private async _getLibraries(resourceId: string): Promise<Library[]> {
if (!resourceId) {
return Promise.reject("Invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("Cluster client is not initialized yet");
}
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Fetching libraries...`
);
try {
const libraries = await this.container.sparkClusterManager.getLibrariesAsync(resourceId);
return libraries;
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch libraries. Reason: ${JSON.stringify(e)}`
);
Logger.logError(e, "Explorer/_getLibraries");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _addLibrary(resourceId: string, library: Library): Promise<void> {
if (!library || !resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
TelemetryProcessor.traceStart(Action.LibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "LibraryManagePane/_deleteLibrary",
libraryName: library.name
});
const libraryName = library.name;
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Uploading ${libraryName}...`
);
try {
await this.container.sparkClusterManager.addLibraryAsync(resourceId, libraryName, library);
TelemetryProcessor.traceSuccess(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName: library.name
});
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to upload ${libraryName}. Reason: ${JSON.stringify(e)}`
);
TelemetryProcessor.traceFailure(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName: library.name,
error: e
});
Logger.logError(e, "Explorer/_uploadLibrary");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
private async _deleteLibrary(resourceId: string, libraryName: string): Promise<void> {
if (!libraryName || !resourceId) {
return Promise.reject("invalid inputs");
}
if (!this.container.sparkClusterManager) {
return Promise.reject("cluster client is not initialized yet");
}
TelemetryProcessor.traceStart(Action.LibraryManage, {
resourceId,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
area: "LibraryManagePane/_deleteLibrary",
libraryName
});
const inProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting ${libraryName}...`
);
try {
await this.container.sparkClusterManager.deleteLibraryAsync(resourceId, libraryName);
TelemetryProcessor.traceSuccess(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName
});
} catch (e) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete ${libraryName}. Reason: ${JSON.stringify(e)}`
);
TelemetryProcessor.traceFailure(Action.LibraryManage, {
resourceId,
area: "LibraryManagePane/_deleteLibrary",
libraryName,
error: e
});
Logger.logError(e, "Explorer/_deleteLibrary");
throw e;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressId);
}
}
}

View File

@@ -19,8 +19,6 @@ import BrowseQueriesPaneTemplate from "./BrowseQueriesPane.html";
import UploadFilePaneTemplate from "./UploadFilePane.html";
import StringInputPaneTemplate from "./StringInputPane.html";
import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
import LibraryManagePaneTemplate from "./LibraryManagePane.html";
import ClusterLibraryPaneTemplate from "./ClusterLibraryPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html";
export class PaneComponent {
@@ -218,24 +216,6 @@ export class SetupNotebooksPaneComponent {
}
}
export class LibraryManagePaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: LibraryManagePaneTemplate
};
}
}
export class ClusterLibraryPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: ClusterLibraryPaneTemplate
};
}
}
export class GitHubReposPaneComponent {
constructor() {
return {