Compare commits

...

21 Commits

Author SHA1 Message Date
sunilyadav840
2ff5efcea4 Fixed eslint issue of githuvClient and NotebookRenderer files 2021-10-05 13:29:21 +05:30
Karthik chakravarthy
f968f57543 Allocation of container on demand (#1097)
* Allocation of container only on demand

* git notebook click emits connect to container and refresh notebooks

* git cleanup local repo

* Reconnect rename and memory heartbeat change

* Fix new notebook click event in case of git login

* Mongo proxy test replace with master file

* code refactor

* format check

* compile errors resolved

* address review comments

* rename environment to workspace

* Added content html rendering in Dialog

* format issue

* Added contentHtml in the dialog which renders jsx element
2021-09-30 12:53:33 -04:00
vaidankarswapnil
6ca8e3c6f4 Fix execute Query 'Results' and 'Query status' section controls gets truncated at 200% resize mode of 'Query1' blade (#1105)
* Fix a11y querytab results section zoom section issue

* Update test smapshot

* Update test snapshot

* Resolved test case issue
2021-09-27 11:03:01 -07:00
vaidankarswapnil
f7e7240010 Fix keyboard focus does not retain on 'New Database' button after closing the 'New Database' blade via ESC key (#1109)
* Fix a11y new database button focus issue

* Update test snapshot and other issues
2021-09-23 08:13:18 -07:00
Sunil Kumar Yadav
acc095a482 fixed graph input query JAWS screen reader issue (#1108) 2021-09-23 08:11:46 -07:00
Hardikkumar Nai
288e6410f3 Choose Column: Aria Position set is not defined for check box present under choose column dialog box. (#1104) 2021-09-23 07:59:10 -07:00
Hardikkumar Nai
9ac3392271 Scale and Settings: 'Learn more' link is not adapting the high contrast mode after applying high contrast theme. (#1103) 2021-09-23 07:58:41 -07:00
Sunil Kumar Yadav
9a908dde9a fixed graph explorer visibility and graph expand keyboard accessibility issue (#1092)
* fixed graph explorer visibility issue

* fixed graph expand keyboard accessibility issue
2021-09-23 07:57:42 -07:00
siddjoshi-ms
ddd2e63fe7 Telemetry added for calculate cost function (#1018)
* Added telemetry for sql cost calculation
2021-09-22 09:49:45 -07:00
Hardikkumar Nai
34c59b4872 Scale and Settings: Text content of 'Info status message' and 'Warning' message is not visible properly at high contrast black mode. (#1090) 2021-09-22 07:40:18 -07:00
Jordi Bunster
7d28af4fc7 Make these required fields (#1101) 2021-09-21 15:50:44 -07:00
Sunil Kumar Yadav
50b99ceb42 fixed horizontal scrollbar issue on 400% resize mode (#1099) 2021-09-21 09:06:49 -07:00
Sunil Kumar Yadav
15a26d6500 fixed notification content visible issue on 400 resize mode (#1098) 2021-09-21 09:06:17 -07:00
Sunil Kumar Yadav
a8150af269 fixed incorrect notification console expand collaped screenreader issue (#1095) 2021-09-21 09:04:47 -07:00
Sunil Kumar Yadav
6a9a0156a3 fixed entity choose column role accessibility issue (#1088) 2021-09-21 09:02:04 -07:00
vaidankarswapnil
ead28f043f Fix after activating "Refresh tree" button, 'Querying database' message appears but screen reader does not provide any information about it (#1091)
* Fix a11y refresh tree querying database msg

* Update test snapshot issue
2021-09-21 09:00:28 -07:00
vaidankarswapnil
b05e5a2145 Fix delete database warning message is not announced by the screen reader after selecting 'Delete Database' menu item (#1074)
* Fix a11y delete database confirmation ississue

* Resolved lint issue - Removed Unnecessary semicolon

* Resolved compilation issue for extractFeature.ts and update test snapshot issue

Co-authored-by: Armando Trejo Oliver <ato9000@users.noreply.github.com>
2021-09-21 08:58:03 -07:00
Hardikkumar Nai
5e8aa491ba Load Graph: Name, role and state properties are not defined for 'full screen graph view' control present under 'Graph' tab section. (#1083) 2021-09-21 08:57:08 -07:00
Hardikkumar Nai
a730c08292 New SQL Query: Luminosity contrast ratio is 2.6:1 which is less than 3:1 for Close(X) icon button of Query 1 tab control (#1089) 2021-09-21 08:56:16 -07:00
Hardikkumar Nai
3dce5cd129 Tooltip is not provided for 'More' control present on the page. (#1093) 2021-09-21 08:54:44 -07:00
siddjoshi-ms
7c186c3ef2 Update GraphAPICompute.rp.ts (#1065) 2021-09-20 15:13:07 -07:00
57 changed files with 1374 additions and 880 deletions

View File

@@ -106,7 +106,6 @@ src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabComponents.ts
src/Explorer/Tabs/TabsBase.ts src/Explorer/Tabs/TabsBase.ts
src/Explorer/Tabs/TriggerTab.ts src/Explorer/Tabs/TriggerTab.ts
src/Explorer/Tabs/UserDefinedFunctionTab.ts
src/Explorer/Tree/AccessibleVerticalList.ts src/Explorer/Tree/AccessibleVerticalList.ts
src/Explorer/Tree/Collection.ts src/Explorer/Tree/Collection.ts
src/Explorer/Tree/ConflictId.ts src/Explorer/Tree/ConflictId.ts
@@ -118,14 +117,12 @@ src/Explorer/Tree/TreeComponents.ts
src/Explorer/Tree/Trigger.ts src/Explorer/Tree/Trigger.ts
src/Explorer/WaitsForTemplateViewModel.ts src/Explorer/WaitsForTemplateViewModel.ts
src/GitHub/GitHubClient.test.ts src/GitHub/GitHubClient.test.ts
src/GitHub/GitHubClient.ts
src/GitHub/GitHubConnector.ts src/GitHub/GitHubConnector.ts
src/GitHub/GitHubOAuthService.ts src/GitHub/GitHubOAuthService.ts
src/Index.ts src/Index.ts
src/Juno/JunoClient.test.ts src/Juno/JunoClient.test.ts
src/Juno/JunoClient.ts src/Juno/JunoClient.ts
src/Platform/Hosted/Authorization.ts src/Platform/Hosted/Authorization.ts
src/ReactDevTools.ts
src/Shared/Constants.ts src/Shared/Constants.ts
src/Shared/DefaultExperienceUtility.test.ts src/Shared/DefaultExperienceUtility.test.ts
src/Shared/DefaultExperienceUtility.ts src/Shared/DefaultExperienceUtility.ts
@@ -157,7 +154,6 @@ src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
src/Explorer/Notebook/NotebookComponent/contents/index.tsx src/Explorer/Notebook/NotebookComponent/contents/index.tsx
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx
src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx

View File

@@ -1,16 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"> xmlns:xlink="http://www.w3.org/1999/xlink">
<path <path
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z" d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
transform="scale(0.5)" transform="scale(0.5)" fill="#000" stroke="#000">
fill="#000"
stroke="#CCC"
>
</path> </path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 503 B

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -2357,6 +2357,8 @@ a:link {
height: 100%; height: 100%;
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
min-height: 300px;
overflow-y: scroll;
} }
.tabs { .tabs {
@@ -2832,6 +2834,8 @@ a:link {
#explorerNotificationConsole { #explorerNotificationConsole {
z-index: 1000; z-index: 1000;
overflow-y: auto;
overflow-x: clip;
} }
.uniqueIndexesContainer { .uniqueIndexesContainer {

View File

@@ -339,9 +339,11 @@ export enum ConflictOperationType {
} }
export enum ConnectionStatusType { export enum ConnectionStatusType {
Connect = "Connect",
Connecting = "Connecting", Connecting = "Connecting",
Connected = "Connected", Connected = "Connected",
Failed = "Connection Failed", Failed = "Connection Failed",
ReConnect = "Reconnect",
} }
export const EmulatorMasterKey = export const EmulatorMasterKey =
@@ -353,15 +355,32 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
export class Notebook { export class Notebook {
public static readonly defaultBasePath = "./notebooks"; public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 5000; public static readonly heartbeatDelayMs = 60000;
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 120000;
public static readonly memoryGuageToGB = 1048576;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it."; public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg = public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation."; "We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg = public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation."; "We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
public static saveNotebookModalTitle = "Save Notebook in temporary workspace";
public static saveNotebookModalContent =
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
public static newNotebookModalTitle = "Create Notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
public static newNotebookModalContent1 =
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
public static newNotebookModalContent2 =
"To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends. ";
public static galleryNotebookDownloadContent1 =
"To download, run, and make changes to this sample notebook, a temporary workspace will be created. When the session expires, any notebooks in the workspace will be removed.";
public static galleryNotebookDownloadContent2 =
"To save your work permanently, save your notebooks to a GitHub repository or download the Notebooks to your local machine before the session ends. ";
public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
public static learnMore = "Learn more.";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -13,6 +13,7 @@ import {
Link, Link,
PrimaryButton, PrimaryButton,
ProgressIndicator, ProgressIndicator,
Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import React, { FC } from "react"; import React, { FC } from "react";
@@ -30,6 +31,7 @@ export interface DialogState {
onOk: () => void, onOk: () => void,
cancelLabel: string, cancelLabel: string,
onCancel: () => void, onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps, choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps, textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean primaryButtonDisabled?: boolean
@@ -58,6 +60,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
onOk: () => void, onOk: () => void,
cancelLabel: string, cancelLabel: string,
onCancel: () => void, onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps, choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps, textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean primaryButtonDisabled?: boolean
@@ -76,6 +79,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
get().closeDialog(); get().closeDialog();
onCancel && onCancel(); onCancel && onCancel();
}, },
contentHtml,
choiceGroupProps, choiceGroupProps,
textFieldProps, textFieldProps,
primaryButtonDisabled, primaryButtonDisabled,
@@ -124,6 +128,7 @@ export interface DialogProps {
type?: DialogType; type?: DialogType;
showCloseButton?: boolean; showCloseButton?: boolean;
onDismiss?: () => void; onDismiss?: () => void;
contentHtml?: JSX.Element;
} }
const DIALOG_MIN_WIDTH = "400px"; const DIALOG_MIN_WIDTH = "400px";
@@ -150,6 +155,7 @@ export const Dialog: FC = () => {
type, type,
showCloseButton, showCloseButton,
onDismiss, onDismiss,
contentHtml,
} = props || {}; } = props || {};
const dialogProps: IDialogProps = { const dialogProps: IDialogProps = {
@@ -191,6 +197,7 @@ export const Dialog: FC = () => {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" /> {linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link> </Link>
)} )}
{contentHtml && <Text>{contentHtml}</Text>}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />} {progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} /> <PrimaryButton {...primaryButtonProps} />

View File

@@ -8,6 +8,9 @@
.input-type-head-text-field { .input-type-head-text-field {
width: 100%; width: 100%;
} }
.input-query-form {
width: 100%;
}
textarea { textarea {
width: 100%; width: 100%;
line-height: 1; line-height: 1;

View File

@@ -160,18 +160,21 @@ export class InputTypeaheadComponent extends React.Component<
return ( return (
<div className="input-typeahead-container"> <div className="input-typeahead-container">
<Stack horizontal> <Stack horizontal>
<TextField <form aria-labelledby="input" className="input-query-form">
multiline={useTextarea} <TextField
rows={1} multiline={useTextarea}
defaultValue={defaultValue} rows={1}
ariaLabel="Input query" id="input"
placeholder={placeholder} defaultValue={defaultValue}
className="input-type-head-text-field" ariaLabel="Input query"
value={defaultValue} placeholder={placeholder}
onKeyDown={this.onSubmit} className="input-type-head-text-field"
onFocus={() => this.setState({ isSuggestionVisible: true })} value={defaultValue}
onChange={(_event, newValue?: string) => this.handleChange(newValue)} onKeyDown={this.onSubmit}
/> onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
/>
</form>
{this.props.showCancelButton && ( {this.props.showCancelButton && (
<IconButton <IconButton
styles={iconButtonStyles} styles={iconButtonStyles}

View File

@@ -7,16 +7,22 @@ exports[`inputTypeahead renders <input /> 1`] = `
<Stack <Stack
horizontal={true} horizontal={true}
> >
<StyledTextFieldBase <form
ariaLabel="Input query" aria-labelledby="input"
className="input-type-head-text-field" className="input-query-form"
multiline={false} >
onChange={[Function]} <StyledTextFieldBase
onFocus={[Function]} ariaLabel="Input query"
onKeyDown={[Function]} className="input-type-head-text-field"
placeholder="placeholder" id="input"
rows={1} multiline={false}
/> onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</form>
</Stack> </Stack>
</div> </div>
`; `;
@@ -28,16 +34,22 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
<Stack <Stack
horizontal={true} horizontal={true}
> >
<StyledTextFieldBase <form
ariaLabel="Input query" aria-labelledby="input"
className="input-type-head-text-field" className="input-query-form"
multiline={true} >
onChange={[Function]} <StyledTextFieldBase
onFocus={[Function]} ariaLabel="Input query"
onKeyDown={[Function]} className="input-type-head-text-field"
placeholder="placeholder" id="input"
rows={1} multiline={true}
/> onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</form>
</Stack> </Stack>
</div> </div>
`; `;

View File

@@ -17,6 +17,8 @@ import Explorer from "../../Explorer";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2"; import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper"; import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer"; import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useNotebook } from "../../Notebook/useNotebook";
import { Dialog, TextFieldProps, useDialog } from "../Dialog"; import { Dialog, TextFieldProps, useDialog } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
@@ -146,7 +148,9 @@ export class NotebookViewerComponent
<NotebookMetadataComponent <NotebookMetadataComponent
data={this.state.galleryItem} data={this.state.galleryItem}
isFavorite={this.state.isFavorite} isFavorite={this.state.isFavorite}
downloadButtonText={this.props.container && "Download to my notebooks"} downloadButtonText={
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
}
onTagClick={this.props.onTagClick} onTagClick={this.props.onTagClick}
onFavoriteClick={this.favoriteItem} onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem} onUnfavoriteClick={this.unfavoriteItem}

View File

@@ -65,7 +65,7 @@ export interface PriceBreakdown {
currencySign: string; currencySign: string;
} }
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } }; export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: { label: {
@@ -272,7 +272,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement"> <Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "} and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
<a href={Urls.autoscaleMigration}>Learn more</a> <Link href={Urls.autoscaleMigration}>Learn more</Link>
</Text> </Text>
); );

View File

@@ -8,6 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -20,6 +20,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -39,6 +40,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -73,6 +75,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -80,11 +83,11 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
> >
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<a <StyledLinkBase
href="https://aka.ms/cosmos-autoscale-migration" href="https://aka.ms/cosmos-autoscale-migration"
> >
Learn more Learn more
</a> </StyledLinkBase>
</Text> </Text>
</StyledMessageBar> </StyledMessageBar>
<StyledChoiceGroup <StyledChoiceGroup
@@ -186,6 +189,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -460,6 +464,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -16,6 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -136,6 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -412,6 +413,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -952,6 +954,7 @@ exports[`SubSettingsComponent renders 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -1228,6 +1231,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -159,6 +159,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -166,16 +167,17 @@ exports[`SettingsUtils functions render 1`] = `
> >
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<a <StyledLinkBase
href="https://aka.ms/cosmos-autoscale-migration" href="https://aka.ms/cosmos-autoscale-migration"
> >
Learn more Learn more
</a> </StyledLinkBase>
</Text> </Text>
<Text <Text
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -195,6 +197,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -207,6 +210,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -219,6 +223,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -230,6 +235,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -249,6 +255,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -264,6 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -278,6 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -291,6 +300,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -302,6 +312,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -321,6 +332,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -363,6 +375,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -378,6 +391,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }
@@ -394,6 +408,7 @@ exports[`SettingsUtils functions render 1`] = `
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"color": "windowtext",
"fontSize": 14, "fontSize": 14,
}, },
} }

View File

@@ -1,7 +1,6 @@
@import "../../../../less/Common/Constants"; @import "../../../../less/Common/Constants";
.tabComponentContainer { .tabComponentContainer {
overflow: hidden;
height: 100%; height: 100%;
.flex-display(); .flex-display();
.flex-direction(); .flex-direction();

View File

@@ -243,6 +243,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}> <div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
<IconButton <IconButton
name="More" name="More"
title="More"
className="treeMenuEllipsis" className="treeMenuEllipsis"
ariaLabel={menuItemLabel} ariaLabel={menuItemLabel}
menuIconProps={{ menuIconProps={{

View File

@@ -211,6 +211,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
}, },
} }
} }
title="More"
/> />
</div> </div>
</div> </div>
@@ -423,6 +424,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
}, },
} }
} }
title="More"
/> />
</div> </div>
</div> </div>

View File

@@ -1,9 +1,11 @@
import { Link } from "@fluentui/react/lib/Link";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
@@ -11,6 +13,7 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient"; import { QueriesClient } from "../Common/QueriesClient";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
@@ -163,23 +166,10 @@ export default class Explorer {
useNotebook.subscribe( useNotebook.subscribe(
async () => { async () => {
if (!this.notebookManager) { this.initiateAndRefreshNotebookList();
const NotebookManager = await ( useNotebook.getState().setIsRefreshed(false);
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
).default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
}, },
(state) => state.isNotebookEnabled (state) => state.isNotebookEnabled || state.isRefreshed
); );
this.resourceTree = new ResourceTreeAdapter(this); this.resourceTree = new ResourceTreeAdapter(this);
@@ -212,6 +202,23 @@ export default class Explorer {
this.refreshExplorer(); this.refreshExplorer();
} }
public async initiateAndRefreshNotebookList(): Promise<void> {
if (!this.notebookManager) {
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
.default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
}
public openEnableSynapseLinkDialog(): void { public openEnableSynapseLinkDialog(): void {
const addSynapseLinkDialogProps: DialogProps = { const addSynapseLinkDialogProps: DialogProps = {
linkProps: { linkProps: {
@@ -345,23 +352,7 @@ export default class Explorer {
return; return;
} }
this._isInitializingNotebooks = true; this._isInitializingNotebooks = true;
if (userContext.features.phoenix) { if (userContext.features.phoenix === false) {
const provisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
resourceId: userContext.databaseAccount.id,
dbAccountName: userContext.databaseAccount.name,
aadToken: userContext.authorizationToken,
resourceGroup: userContext.resourceGroup,
subscriptionId: userContext.subscriptionId,
};
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
}
} else {
await this.ensureNotebookWorkspaceRunning(); await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo( const connectionInfo = await listConnectionInfo(
userContext.subscriptionId, userContext.subscriptionId,
@@ -376,13 +367,59 @@ export default class Explorer {
}); });
} }
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.refreshNotebookList(); this.refreshNotebookList();
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
const provisionData = {
aadToken: userContext.authorizationToken,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
try {
useNotebook.getState().setIsAllocating(true);
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (
connectionInfo.status === HttpStatusCodes.OK &&
connectionInfo.data &&
connectionInfo.data.notebookServerUrl
) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
useNotebook.getState().setIsAllocating(false);
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
}
} catch (error) {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
throw error;
}
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
public resetNotebookWorkspace(): void { public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError( handleError(
@@ -654,6 +691,9 @@ export default class Explorer {
if (!notebookContentItem || !notebookContentItem.path) { if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
} }
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
this.allocateContainer();
}
const notebookTabs = useTabs const notebookTabs = useTabs
.getState() .getState()
@@ -875,9 +915,51 @@ export default class Explorer {
handleError(error, "Explorer/onNewNotebookClicked"); handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error); throw new Error(error);
} }
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
if (isPhoenixEnabled) {
if (isGithubTree) {
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
};
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
parent = parent || this.resourceTree.myNotebooksContentRoot; private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
@@ -924,7 +1006,26 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
} }
public openNotebookTerminal(kind: ViewModels.TerminalKind): void { public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (NotebookUtil.isPhoenixEnabled()) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
);
}
} else {
this.connectToNotebookTerminal(kind);
}
}
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
let title: string; let title: string;
switch (kind) { switch (kind) {
@@ -975,7 +1076,7 @@ export default class Explorer {
notebookUrl?: string, notebookUrl?: string,
galleryItem?: IGalleryItem, galleryItem?: IGalleryItem,
isFavorite?: boolean isFavorite?: boolean
) { ): Promise<void> {
const title = "Gallery"; const title = "Gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = useTabs const galleryTab = useTabs
@@ -1079,7 +1180,27 @@ export default class Explorer {
} }
public openUploadFilePanel(parent?: NotebookContentItem): void { public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot; if (NotebookUtil.isPhoenixEnabled()) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
@@ -1088,6 +1209,24 @@ export default class Explorer {
); );
} }
public getDownloadModalConent(fileName: string): JSX.Element {
if (NotebookUtil.isPhoenixEnabled()) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
<br />
<p>
{Notebook.galleryNotebookDownloadContent2}
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
}
public async refreshExplorer(): Promise<void> { public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()

View File

@@ -1,8 +1,8 @@
import * as React from "react"; import * as React from "react";
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg"; import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
import ExpandIcon from "../../../../images/Expand_14x14.svg"; import ExpandIcon from "../../../../images/Expand_14x14.svg";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
interface MiddlePaneComponentProps { interface MiddlePaneComponentProps {
isTabsContentExpanded: boolean; isTabsContentExpanded: boolean;
@@ -17,7 +17,14 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
<div className="middlePane"> <div className="middlePane">
<div className="graphTitle"> <div className="graphTitle">
<span className="paneTitle">Graph</span> <span className="paneTitle">Graph</span>
<span className="graphExpandCollapseBtn pull-right" onClick={this.props.toggleExpandGraph}> <span
className="graphExpandCollapseBtn pull-right"
onClick={this.props.toggleExpandGraph}
role="button"
aria-expanded={this.props.isTabsContentExpanded}
aria-name="View graph in full screen"
tabIndex={0}
>
<img <img
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon} src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"} alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import { useTabs } from "../../../hooks/useTabs";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil"; import * as CommandBarUtil from "./CommandBarUtil";
@@ -55,15 +56,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if ( if (NotebookUtil.isPhoenixEnabled()) {
userContext.features.notebooksTemporarilyDown === false && uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
userContext.features.phoenix === true &&
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus"));
} }
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { if (
userContext.features.phoenix === false &&
userContext.features.notebooksTemporarilyDown === false &&
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
} }

View File

@@ -596,7 +596,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
return { return {
iconSrc: GitHubIcon, iconSrc: GitHubIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => onCommandClick: () => {
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
@@ -606,7 +606,8 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
gitHubClientProp={container.notebookManager.gitHubClient} gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient} junoClientProp={junoClient}
/> />
), );
},
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: false, disabled: false,

View File

@@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { ConnectionStatus } from "./ConnectionStatusComponent"; import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent"; import { MemoryTracker } from "./MemoryTrackerComponent";
@@ -203,9 +204,9 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
}; };
}; };
export const createConnectionStatus = (key: string): ICommandBarItemProps => { export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
return { return {
key, key,
onRender: () => <ConnectionStatus />, onRender: () => <ConnectionStatus container={container} />,
}; };
}; };

View File

@@ -3,77 +3,182 @@
.connectionStatusContainer { .connectionStatusContainer {
cursor: default; cursor: default;
align-items: center; align-items: center;
margin: 0 9px;
border: 1px; border: 1px;
min-height: 44px; min-height: 44px;
> span { > span {
padding-right: 12px; padding-right: 12px;
font-size: 13px; font-size: 12px;
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
color: @DefaultFontColor; color: @DefaultFontColor;
} }
&:focus{
outline: 0px;
}
} }
.connectionStatusFailed{ .commandReactBtn {
color: #bd1919; &:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 1px dashed #605e5c;
}
} }
.ring-container { .connectedReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 0px;
}
}
.connectIcon{
margin: 0px 4px;
height: 18px;
width: 18px;
color: rgb(0, 120, 212);
}
.status {
position: relative; position: relative;
} display: block;
margin-right: 8px;
.ringringGreen { width: 1em;
border: 3px solid green; height: 1em;
border-radius: 30px; font-size: 9px!important;
height: 18px; padding: 0px!important;
width: 18px; border-radius: 0.5em;
}
.status::before,
.status::after {
position: absolute; position: absolute;
margin: .4285em 0em 0em 0.07477em; content: "";
animation: pulsate 3s ease-out; }
animation-iteration-count: infinite;
opacity: 0.0 .status::before {
} top: 0;
.ringringYellow{ left: 0;
border: 3px solid #ffbf00; width: 1em;
border-radius: 30px; height: 1em;
height: 18px; background-color: rgba(#fff, 0.1);
width: 18px; border-radius: 100%;
position: absolute; opacity: 1;
margin: .4285em 0em 0em 0.07477em; transform: translate3d(0, 0, 0) scale(0);
animation: pulsate 3s ease-out; }
animation-iteration-count: infinite;
opacity: 0.0 .connected{
} background-color: green;
.ringringRed{ box-shadow:
border: 3px solid #bd1919; 0 0 0 0em rgba(green, 0),
border-radius: 30px; 0em 0.05em 0.1em rgba(#000000, 0.2);
height: 18px; transform: translate3d(0, 0, 0) scale(1);
width: 18px; }
position: absolute; .connecting{
margin: .4285em 0em 0em 0.07477em; background-color:#ffbf00;
animation: pulsate 3s ease-out; box-shadow:
animation-iteration-count: infinite; 0 0 0 0em rgba(#ffbf00, 0),
opacity: 0.0 0em 0.05em 0.1em rgba(#000000, 0.2);
} transform: translate3d(0, 0, 0) scale(1);
@keyframes pulsate { }
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;} .failed{
15% {opacity: 0.8;} background-color:#bd1919;
25% {opacity: 0.6;} box-shadow:
45% {opacity: 0.4;} 0 0 0 0em rgba(#bd1919, 0),
70% {opacity: 0.3;} 0em 0.05em 0.1em rgba(#000000, 0.2);
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;} transform: translate3d(0, 0, 0) scale(1);
} }
.locationGreenDot{
font-size: 20px; .status.connecting.is-animating {
margin-right: 0.07em; animation: status-outer-connecting 3000ms infinite;
color: green; }
} .status.failed.is-animating {
.locationYellowDot{ animation: status-outer-failed 3000ms infinite;
font-size: 20px; }
margin-right: 0.07em; .status.connected.is-animating {
color: #ffbf00; animation: status-outer-connected 3000ms infinite;
} }
.locationRedDot{ @keyframes status-outer-connected {
font-size: 20px;
margin-right: 0.07em; 0% {
color: #bd1919; transform: translate3d(0, 0, 0) scale(1);
} box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-failed {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-connecting {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}

View File

@@ -1,17 +1,21 @@
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react"; import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
import { ActionButton } from "@fluentui/react/lib/Button";
import * as React from "react"; import * as React from "react";
import { ConnectionStatusType } from "../../../Common/Constants"; import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook"; import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less"; import "../CommandBar/ConnectionStatusComponent.less";
interface Props {
export const ConnectionStatus: React.FC = (): JSX.Element => { container: Explorer;
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const [second, setSecond] = React.useState("00"); const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00"); const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false); const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0); const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("locationYellowDot"); const [statusColor, setStatusColor] = React.useState("");
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow"); const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
const toolTipContent = "Hosted runtime status.";
React.useEffect(() => { React.useEffect(() => {
let intervalId: NodeJS.Timeout; let intervalId: NodeJS.Timeout;
@@ -39,34 +43,65 @@ export const ConnectionStatus: React.FC = (): JSX.Element => {
}; };
const connectionInfo = useNotebook((state) => state.connectionInfo); const connectionInfo = useNotebook((state) => state.connectionInfo);
if (!connectionInfo) { const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
return <></>;
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
if (
connectionInfo &&
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
<span>{connectionInfo.status}</span>
</Stack>
</TooltipHost>
</ActionButton>
);
} }
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) { if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
setIsActive(true); setIsActive(true);
setStatusColor("status connecting is-animating");
setToolTipContent("Connecting to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer(); stopTimer();
setStatusColor("locationGreenDot"); setStatusColor("status connected is-animating");
setStatusColorAnimation("ringringGreen"); setToolTipContent("Connected to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) { } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer(); stopTimer();
setStatusColor("locationRedDot"); setStatusColor("status failed is-animating");
setStatusColorAnimation("ringringRed"); setToolTipContent("Click here to Reconnect to temporary workspace.");
} }
return ( return (
<TooltipHost content={toolTipContent}> <ActionButton
<Stack className="connectionStatusContainer" horizontal> className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
<div className="ring-container"> onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
<div className={statusColorAnimation}></div> connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
<Icon iconName="LocationDot" className={statusColor} /> }
</div> >
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}> <TooltipHost content={toolTipContent}>
{connectionInfo.status} <Stack className="connectionStatusContainer" horizontal>
</span> <i className={statusColor}></i>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && ( <span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
<ProgressIndicator description={minute + ":" + second} /> {connectionInfo.status}
)} </span>
</Stack> {connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
</TooltipHost> <ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
)}
</Stack>
</TooltipHost>
</ActionButton>
); );
}; };

View File

@@ -129,7 +129,7 @@ export class NotificationConsoleComponent extends React.Component<
className="expandCollapseButton" className="expandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")} aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
aria-expanded={!this.props.isConsoleExpanded} aria-expanded={!this.props.isConsoleExpanded}
> >
<img <img
@@ -205,7 +205,9 @@ export class NotificationConsoleComponent extends React.Component<
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />} {item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />} {item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
<span className="date">{item.date}</span> <span className="date">{item.date}</span>
<span className="message">{item.message}</span> <span className="message" role="alert" aria-live="assertive">
{item.message}
</span>
</div> </div>
)); ));
} }

View File

@@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</div> </div>
<div <div
aria-expanded={true} aria-expanded={true}
aria-label="console button expanded" aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
@@ -236,7 +236,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</div> </div>
<div <div
aria-expanded={true} aria-expanded={true}
aria-label="console button expanded" aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
@@ -340,7 +340,9 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
date date
</span> </span>
<span <span
aria-live="assertive"
className="message" className="message"
role="alert"
> >
message message
</span> </span>

View File

@@ -2,12 +2,15 @@
* Notebook container related stuff * Notebook container related stuff
*/ */
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook"; import { useNotebook } from "./useNotebook";
export class NotebookContainerClient { export class NotebookContainerClient {
@@ -42,7 +45,7 @@ export class NotebookContainerClient {
}, delayMs); }, delayMs);
} }
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> { public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo; const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected"; const error = "No server endpoint detected";
@@ -75,6 +78,12 @@ export class NotebookContainerClient {
freeKB: memoryUsageInfo.free, freeKB: memoryUsageInfo.free,
}; };
} }
} else if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.ReConnect,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
} }
return undefined; return undefined;
} catch (error) { } catch (error) {
@@ -84,6 +93,13 @@ export class NotebookContainerClient {
"Connection lost with Notebook server. Attempting to reconnect..." "Connection lost with Notebook server. Attempting to reconnect..."
); );
} }
if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
}
this.onConnectionLost(); this.onConnectionLost();
return undefined; return undefined;
} }

View File

@@ -212,6 +212,7 @@ export default class NotebookManager {
"Cancel", "Cancel",
() => reject(new Error("Commit dialog canceled")), () => reject(new Error("Commit dialog canceled")),
undefined, undefined,
undefined,
{ {
label: "Commit message", label: "Commit message",
autoAdjustHeight: true, autoAdjustHeight: true,

View File

@@ -31,12 +31,13 @@ import StatusBar from "./StatusBar";
import CellToolbar from "./Toolbar"; import CellToolbar from "./Toolbar";
export interface NotebookRendererBaseProps { export interface NotebookRendererBaseProps {
contentRef: any; contentRef: ContentRef;
} }
interface NotebookRendererDispatchProps { interface NotebookRendererDispatchProps {
storeNotebookSnapshot: (imageSrc: string, requestId: string) => void; storeNotebookSnapshot: (imageSrc: string, requestId: string) => void;
notebookSnapshotError: (error: string) => void; notebookSnapshotError: (error: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
} }
interface StateProps { interface StateProps {
@@ -73,7 +74,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() { componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) { if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any); loadTransform(this.props as NotebookRendererProps);
} }
} }
@@ -138,7 +139,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
}} }}
</CodeCell> </CodeCell>
), ),
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => markdown: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) =>
decorate( decorate(
id, id,
contentRef, contentRef,
@@ -155,7 +156,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
</MarkdownCell> </MarkdownCell>
), ),
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => raw: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) =>
decorate( decorate(
id, id,
contentRef, contentRef,
@@ -202,7 +203,8 @@ export const makeMapStateToProps = (
return mapStateToProps; return mapStateToProps;
}; };
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererBaseProps) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
const makeMapDispatchToProps = (_initialDispatch: Dispatch, _initialProps: NotebookRendererBaseProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => { const mapDispatchToProps = (dispatch: Dispatch) => {
return { return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => addTransform: (transform: React.ComponentType & { MIMETYPE: string }) =>

View File

@@ -3,6 +3,7 @@ import { AppState, selectors } from "@nteract/core";
import domtoimage from "dom-to-image"; import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas"; import Html2Canvas from "html2canvas";
import path from "path"; import path from "path";
import { userContext } from "../../UserContext";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as StringUtils from "../../Utils/StringUtils"; import * as StringUtils from "../../Utils/StringUtils";
import { SnapshotFragment } from "./NotebookComponent/types"; import { SnapshotFragment } from "./NotebookComponent/types";
@@ -328,4 +329,16 @@ export class NotebookUtil {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
} }
public static getNotebookBtnTitle(fileName: string): string {
if (this.isPhoenixEnabled()) {
return `Download to ${fileName}`;
} else {
return `Download to my notebooks`;
}
}
public static isPhoenixEnabled(): boolean {
return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true;
}
} }

View File

@@ -2,10 +2,12 @@ import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -14,6 +16,7 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager"; import NotebookManager from "./NotebookManager";
import { NotebookUtil } from "./NotebookUtil";
interface NotebookState { interface NotebookState {
isNotebookEnabled: boolean; isNotebookEnabled: boolean;
@@ -28,8 +31,10 @@ interface NotebookState {
myNotebooksContentRoot: NotebookContentItem; myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem;
connectionInfo: DataModels.ContainerConnectionInfo; connectionInfo: ContainerConnectionInfo;
notebookFolderName: string; notebookFolderName: string;
isAllocating: boolean;
isRefreshed: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -46,7 +51,10 @@ interface NotebookState {
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void; setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
setIsAllocating: (isAllocating: boolean) => void;
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
setIsRefreshed: (isAllocating: boolean) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -69,8 +77,12 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
myNotebooksContentRoot: undefined, myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined, galleryContentRoot: undefined,
connectionInfo: undefined, connectionInfo: {
status: ConnectionStatusType.Connect,
},
notebookFolderName: undefined, notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -175,7 +187,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
}, },
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => { initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks"; const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName }); set({ notebookFolderName });
const myNotebooksContentRoot = { const myNotebooksContentRoot = {
name: get().notebookFolderName, name: get().notebookFolderName,
@@ -256,5 +268,15 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ gitHubNotebooksContentRoot }); set({ gitHubNotebooksContentRoot });
} }
}, },
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }), setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: undefined,
authToken: undefined,
});
useNotebook.getState().setIsAllocating(false);
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
})); }));

View File

@@ -23,10 +23,12 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
export interface AddDatabasePaneProps { export interface AddDatabasePaneProps {
explorer: Explorer; explorer: Explorer;
buttonElement?: HTMLElement;
} }
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
explorer: container, explorer: container,
buttonElement,
}: AddDatabasePaneProps) => { }: AddDatabasePaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
let throughput: number; let throughput: number;
@@ -77,6 +79,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
}; };
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage); TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
buttonElement.focus();
}, []); }, []);
const onSubmit = () => { const onSubmit = () => {

View File

@@ -198,6 +198,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<TextField <TextField
aria-required="true" aria-required="true"
required={true}
autoComplete="off" autoComplete="off"
styles={getTextFieldStyles()} styles={getTextFieldStyles()}
pattern="[^/?#\\]*[^/?# \\]" pattern="[^/?#\\]*[^/?# \\]"
@@ -285,6 +286,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
underlined underlined
styles={getTextFieldStyles({ fontSize: 12, width: 150 })} styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true" aria-required="true"
required={true}
ariaLabel="addCollection-tableId" ariaLabel="addCollection-tableId"
autoComplete="off" autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]" pattern="[^/?#\\]*[^/?# \\]"

View File

@@ -77,7 +77,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.repo selectedLocation.repo
)} - ${selectedLocation.branch}`; )} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) { } else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
destination = "My Notebooks Scratch"; destination = useNotebook.getState().notebookFolderName;
} }
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);

View File

@@ -9,6 +9,7 @@
flex-grow: 1; flex-grow: 1;
padding: 0 34px; padding: 0 34px;
margin: 20px 0; margin: 20px 0;
overflow-x: hidden;
& > :not(.collapsibleSection) { & > :not(.collapsibleSection) {
margin-bottom: @DefaultSpace; margin-bottom: @DefaultSpace;

View File

@@ -33,7 +33,13 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center"> <Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon} {icon}
<span className="panelWarningErrorDetailsLinkContainer"> <span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small" aria-label="message"> <Text
role="alert"
aria-live="assertive"
aria-label={message}
className="panelWarningErrorMessage"
variant="small"
>
{message} {message}
{link && linkText && ( {link && linkText && (
<Link target="_blank" href={link}> <Link target="_blank" href={link}>

View File

@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
it("should render error in header", () => { it("should render error in header", () => {
render(<RightPaneForm {...props} formError="file already Exist" />); render(<RightPaneForm {...props} formError="file already Exist" />);
expect(screen.getByLabelText("error")).toBeDefined(); expect(screen.getByLabelText("error")).toBeDefined();
expect(screen.getByLabelText("message").innerHTML).toEqual("file already Exist"); expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
}); });
}); });

View File

@@ -29,6 +29,7 @@ describe("Table query select Panel", () => {
it("Should checked availableCheckbox by default", () => { it("Should checked availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />); const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({ expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
ariaPositionInSet: 0,
id: "availableCheckbox", id: "availableCheckbox",
label: "Available Columns", label: "Available Columns",
checked: true, checked: true,

View File

@@ -128,8 +128,9 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
label="Available Columns" label="Available Columns"
checked={isAvailableColumnChecked} checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick} onChange={availableColumnsCheckboxClick}
ariaPositionInSet={0}
/> />
{columnOptions.map((column) => { {columnOptions.map((column, index) => {
return ( return (
<Checkbox <Checkbox
label={column.columnName} label={column.columnName}
@@ -137,6 +138,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
key={column.columnName} key={column.columnName}
checked={column.selected} checked={column.selected}
disabled={!column.editable} disabled={!column.editable}
ariaPositionInSet={index + 1}
/> />
); );
})} })}

View File

@@ -37,12 +37,14 @@ exports[`Table query select Panel should render Default properly 1`] = `
className="column-select-view" className="column-select-view"
> >
<StyledCheckboxBase <StyledCheckboxBase
ariaPositionInSet={0}
checked={true} checked={true}
id="availableCheckbox" id="availableCheckbox"
label="Available Columns" label="Available Columns"
onChange={[Function]} onChange={[Function]}
> >
<CheckboxBase <CheckboxBase
ariaPositionInSet={0}
checked={true} checked={true}
id="availableCheckbox" id="availableCheckbox"
label="Available Columns" label="Available Columns"
@@ -328,6 +330,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
<input <input
aria-checked="true" aria-checked="true"
aria-label="Available Columns" aria-label="Available Columns"
aria-posinset={0}
checked={true} checked={true}
className="input-55" className="input-55"
data-ktp-execute-target={true} data-ktp-execute-target={true}
@@ -646,6 +649,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
</CheckboxBase> </CheckboxBase>
</StyledCheckboxBase> </StyledCheckboxBase>
<StyledCheckboxBase <StyledCheckboxBase
ariaPositionInSet={1}
checked={true} checked={true}
disabled={false} disabled={false}
key="" key=""
@@ -653,6 +657,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
onChange={[Function]} onChange={[Function]}
> >
<CheckboxBase <CheckboxBase
ariaPositionInSet={1}
checked={true} checked={true}
disabled={false} disabled={false}
label="" label=""
@@ -939,6 +944,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
aria-checked="true" aria-checked="true"
aria-disabled={false} aria-disabled={false}
aria-label="" aria-label=""
aria-posinset={1}
checked={true} checked={true}
className="input-55" className="input-55"
data-ktp-execute-target={true} data-ktp-execute-target={true}

View File

@@ -327,13 +327,17 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
key=".0:$.1" key=".0:$.1"
> >
<Text <Text
aria-label="message" aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
aria-live="assertive"
className="panelWarningErrorMessage" className="panelWarningErrorMessage"
role="alert"
variant="small" variant="small"
> >
<span <span
aria-label="message" aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
aria-live="assertive"
className="panelWarningErrorMessage css-56" className="panelWarningErrorMessage css-56"
role="alert"
> >
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources. Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
</span> </span>

View File

@@ -307,16 +307,23 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
title: "New " + getDatabaseName(), title: "New " + getDatabaseName(),
description: undefined, description: undefined,
onClick: () => onClick: () => this.openAddDatabasePanel(),
useSidePanel
.getState()
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
}); });
} }
return items; return items;
} }
private openAddDatabasePanel() {
const newDatabaseButton = document.activeElement as HTMLElement;
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={newDatabaseButton} />
);
}
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) { private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return { return {
iconSrc: NotebookIcon, iconSrc: NotebookIcon,

View File

@@ -999,7 +999,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
"data-order": 2, "data-order": 2,
"data-title": "Query Stats", "data-title": "Query Stats",
}} }}
style={{ height: "100%" }} style={{ height: "100%", overflowY: "scroll" }}
> >
{this.state.allResultsMetadata.length > 0 && !this.state.error && ( {this.state.allResultsMetadata.length > 0 && !this.state.error && (
<div className="queryMetricsSummaryContainer"> <div className="queryMetricsSummaryContainer">

View File

@@ -47,7 +47,9 @@
<img class="and-or-svg" src="/And-Or.svg" alt="Group selected clauses" /> <img class="and-or-svg" src="/And-Or.svg" alt="Group selected clauses" />
</button> </button>
</th> </th>
<th class="clause-table-cell header-background"><!-- Grouping indicator --></th> <th class="clause-table-cell header-background">
<!-- Grouping indicator -->
</th>
<th class="clause-table-cell header-background and-or-header"> <th class="clause-table-cell header-background and-or-header">
<span data-bind="text: andLabel"></span> <span data-bind="text: andLabel"></span>
</th> </th>
@@ -136,7 +138,7 @@
class="select-options-link" class="select-options-link"
data-bind="click: selectQueryOptions, event: { keydown: onselectQueryOptionsKeyDown }" data-bind="click: selectQueryOptions, event: { keydown: onselectQueryOptionsKeyDown }"
tabindex="0" tabindex="0"
role="link" role="button"
> >
<span>Choose Columns... </span> <span>Choose Columns... </span>
</a> </a>

View File

@@ -128,7 +128,12 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
notebooksTree.children.push(buildGalleryNotebooksTree()); notebooksTree.children.push(buildGalleryNotebooksTree());
} }
if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) { if (
myNotebooksContentRoot &&
((NotebookUtil.isPhoenixEnabled() &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) ||
userContext.features.phoenix === false)
) {
notebooksTree.children.push(buildMyNotebooksTree()); notebooksTree.children.push(buildMyNotebooksTree());
} }
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
@@ -162,7 +167,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
myNotebooksContentRoot, myNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => { container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
@@ -181,7 +190,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
gitHubNotebooksContentRoot, gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => { container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
} }
}); });
@@ -213,23 +226,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}, },
}, },
]; ];
const connectGitContextMenu: TreeNodeMenuItem[] = [ gitHubNotebooksTree.contextMenu = manageGitContextMenu;
{
label: "Connect to GitHub",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
];
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
gitHubNotebooksTree.isExpanded = true; gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true; gitHubNotebooksTree.isAlphaSorted = true;

View File

@@ -45,7 +45,6 @@ import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter { export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks"; public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
public static readonly GitHubReposTitle = "GitHub repos"; public static readonly GitHubReposTitle = "GitHub repos";
private static readonly DataTitle = "DATA"; private static readonly DataTitle = "DATA";

View File

@@ -1,9 +1,9 @@
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import * as UrlUtility from "../Common/UrlUtility"; import * as UrlUtility from "../Common/UrlUtility";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
export interface IGitHubPageInfo { export interface IGitHubPageInfo {
endCursor: string; endCursor: string;
@@ -225,7 +225,7 @@ export class GitHubClient {
private static readonly SelfErrorCode = 599; private static readonly SelfErrorCode = 599;
private ocktokit: Octokit; private ocktokit: Octokit;
constructor(private errorCallback: (error: any) => void) { constructor(private errorCallback: (error: Error) => void) {
this.initOctokit(); this.initOctokit();
} }
@@ -501,10 +501,11 @@ export class GitHubClient {
this.ocktokit = new Octokit({ this.ocktokit = new Octokit({
auth: token, auth: token,
log: { log: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
debug: () => {}, debug: () => {},
info: (message?: any) => GitHubClient.log(Logger.logInfo, message), info: (message?: string) => GitHubClient.log(Logger.logInfo, message),
warn: (message?: any) => GitHubClient.log(Logger.logWarning, message), warn: (message?: string) => GitHubClient.log(Logger.logWarning, message),
error: (error?: any) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit"), error: (error?: Error) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit"),
}, },
}); });
@@ -514,7 +515,7 @@ export class GitHubClient {
}); });
} }
private static log(logger: (message: string, area: string) => void, message?: any) { private static log(logger: (message: string, area: string) => void, message?: string) {
if (message) { if (message) {
message = typeof message === "string" ? message : JSON.stringify(message); message = typeof message === "string" ? message : JSON.stringify(message);
logger(message, "GitHubClient.Octokit"); logger(message, "GitHubClient.Octokit");

View File

@@ -1,7 +1,5 @@
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import { ContainerConnectionInfo } from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
@@ -15,7 +13,6 @@ export interface IPhoenixConnectionInfoResult {
} }
export interface IProvosionData { export interface IProvosionData {
cosmosEndpoint: string; cosmosEndpoint: string;
resourceId: string;
dbAccountName: string; dbAccountName: string;
aadToken: string; aadToken: string;
resourceGroup: string; resourceGroup: string;
@@ -26,11 +23,7 @@ export class PhoenixClient {
provisionData: IProvosionData provisionData: IProvosionData
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> { ): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
try { try {
const connectionStatus: ContainerConnectionInfo = { const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, {
method: "POST", method: "POST",
headers: PhoenixClient.getHeaders(), headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData), body: JSON.stringify(provisionData),
@@ -38,31 +31,20 @@ export class PhoenixClient {
let data: IPhoenixConnectionInfoResult; let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) { if (response.status === HttpStatusCodes.OK) {
data = await response.json(); data = await response.json();
if (data && data.notebookServerUrl) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
}
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().setConnectionInfo(connectionStatus);
} }
return { return {
status: response.status, status: response.status,
data, data,
}; };
} catch (error) { } catch (error) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
console.error(error); console.error(error);
throw error; throw error;
} }
} }
public static getPhoenixEndpoint(): string { public static getPhoenixEndpoint(): string {
const phoenixEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; const phoenixEndpoint =
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) { if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`; const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error); console.error(error);
@@ -73,7 +55,7 @@ export class PhoenixClient {
} }
public getPhoenixContainerPoolingEndPoint(): string { public getPhoenixContainerPoolingEndPoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`; return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
} }
private static getHeaders(): HeadersInit { private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();

View File

@@ -20,6 +20,7 @@ export type Features = {
readonly enableKoResourceTree: boolean; readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly phoenixEndpoint?: string;
readonly livyEndpoint?: string; readonly livyEndpoint?: string;
readonly notebookBasePath?: string; readonly notebookBasePath?: string;
readonly notebookServerToken?: string; readonly notebookServerToken?: string;
@@ -68,6 +69,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
mongoProxyEndpoint: get("mongoproxyendpoint"), mongoProxyEndpoint: get("mongoproxyendpoint"),
mongoProxyAPIs: get("mongoproxyapis"), mongoProxyAPIs: get("mongoproxyapis"),
junoEndpoint: get("junoendpoint"), junoEndpoint: get("junoendpoint"),
phoenixEndpoint: get("phoenixendpoint"),
livyEndpoint: get("livyendpoint"), livyEndpoint: get("livyendpoint"),
notebookBasePath: get("notebookbasepath"), notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"), notebookServerToken: get("notebookservertoken"),
@@ -85,7 +87,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
}; };
} }
export function hasFlag(flags: string, desiredFlag: string): boolean { export function hasFlag(flags: string | undefined, desiredFlag: string | undefined): boolean {
if (!flags || !desiredFlag) { if (!flags || !desiredFlag) {
return false; return false;
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
if (window.parent !== window) { if (window.parent !== window) {
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
} }

View File

@@ -151,10 +151,10 @@ export const getReadRegions = async (): Promise<Array<string>> => {
}); });
if (response.result.location !== undefined) { if (response.result.location !== undefined) {
readRegions.push(response.result.location.replace(" ", "").toLowerCase()); readRegions.push(response.result.location.split(" ").join("").toLowerCase());
} else { } else {
for (const location of response.result.locations) { for (const location of response.result.locations) {
readRegions.push(location.locationName.replace(" ", "").toLowerCase()); readRegions.push(location.locationName.split(" ").join("").toLowerCase());
} }
} }
return readRegions; return readRegions;

View File

@@ -139,6 +139,14 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str
}; };
export const getRegions = async (): Promise<Array<string>> => { export const getRegions = async (): Promise<Array<string>> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getRegions",
description: "",
selfServeClassName: SqlX.name,
};
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
try { try {
const regions = new Array<string>(); const regions = new Array<string>();
@@ -156,8 +164,12 @@ export const getRegions = async (): Promise<Array<string>> => {
regions.push(location.locationName.split(" ").join("").toLowerCase()); regions.push(location.locationName.split(" ").join("").toLowerCase());
} }
} }
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
return regions; return regions;
} catch (err) { } catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
return new Array<string>(); return new Array<string>();
} }
}; };
@@ -167,6 +179,14 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
}; };
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => { export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getPriceMap",
description: "fetch prices API call",
selfServeClassName: SqlX.name,
};
const getPriceMapTimestamp = selfServeTraceStart(telemetryData);
try { try {
const priceMap = new Map<string, Map<string, number>>(); const priceMap = new Map<string, Map<string, number>>();
@@ -192,8 +212,12 @@ export const getPriceMap = async (regions: Array<string>): Promise<Map<string, M
priceMap.set(region, regionPriceMap); priceMap.set(region, regionPriceMap);
} }
selfServeTraceSuccess(telemetryData, getPriceMapTimestamp);
return priceMap; return priceMap;
} catch (err) { } catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapTimestamp);
return undefined; return undefined;
} }
}; };

View File

@@ -1,5 +1,10 @@
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators"; import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import { selfServeTrace } from "../SelfServeTelemetryProcessor"; import {
selfServeTrace,
selfServeTraceFailure,
selfServeTraceStart,
selfServeTraceSuccess,
} from "../SelfServeTelemetryProcessor";
import { import {
ChoiceItem, ChoiceItem,
Description, Description,
@@ -205,6 +210,14 @@ let priceMap: Map<string, Map<string, number>>;
let regions: Array<string>; let regions: Array<string>;
const calculateCost = (skuName: string, instanceCount: number): Description => { const calculateCost = (skuName: string, instanceCount: number): Description => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "calculateCost",
description: "performs final calculation",
selfServeClassName: SqlX.name,
};
const calculateCostTimestamp = selfServeTraceStart(telemetryData);
try { try {
let costPerHour = 0; let costPerHour = 0;
for (const region of regions) { for (const region of regions) {
@@ -215,14 +228,22 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
costPerHour += incrementalCost; costPerHour += incrementalCost;
} }
if (costPerHour === 0) {
throw new Error("Cost per hour = 0");
}
costPerHour *= instanceCount; costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100; costPerHour = Math.round(costPerHour * 100) / 100;
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
return { return {
textTKey: `${costPerHour} USD`, textTKey: `${costPerHour} USD`,
type: DescriptionType.Text, type: DescriptionType.Text,
}; };
} catch (err) { } catch (err) {
const failureTelemetry = { err, regions, priceMap, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
return costPerHourDefaultValue; return costPerHourDefaultValue;
} }
}; };

View File

@@ -1,7 +1,3 @@
import * as Constants from "../Common/Constants";
export const manualToAutoscaleDisclaimer = `The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. <a href="${Constants.Urls.autoscaleMigration}">Learn more</a>.`;
export const minAutoPilotThroughput = 4000; export const minAutoPilotThroughput = 4000;
export const autoPilotIncrementStep = 1000; export const autoPilotIncrementStep = 1000;

View File

@@ -10,6 +10,7 @@ import {
SortBy, SortBy,
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -225,67 +226,89 @@ export function downloadItem(
const name = data.name; const name = data.name;
useDialog.getState().showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
`Download to ${useNotebook.getState().notebookFolderName}`, `Download to ${useNotebook.getState().notebookFolderName}`,
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, undefined,
"Download", "Download",
async () => { async () => {
const clearInProgressMessage = logConsoleProgress( if (NotebookUtil.isPhoenixEnabled()) {
`Downloading ${name} to ${useNotebook.getState().notebookFolderName}` await container.allocateContainer();
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
downloadNotebookItem(name, data, junoClient, container, onComplete);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect to temporary workspace. Please refresh the page and try again."
);
}
},
"Cancel",
undefined,
container.getDownloadModalConent(name)
);
}
export async function downloadNotebookItem(
fileName: string,
data: IGalleryItem,
junoClient: JunoClient,
container: Explorer,
onComplete: (item: IGalleryItem) => void
) {
const clearInProgressMessage = logConsoleProgress(
`Downloading ${fileName} to ${useNotebook.getState().notebookFolderName}`
);
const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
});
try {
const response = await junoClient.getNotebookContent(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
); );
const startKey = traceStart(Action.NotebooksGalleryDownload, { onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
notebookId: data.id, notebookId: data.id,
downloadCount: data.downloads, downloadCount: data.downloads,
isSample: data.isSample, isSample: data.isSample,
}); error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
try { handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
const response = await junoClient.getNotebookContent(data.id); }
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
const notebook = JSON.parse(response.data) as Notebook; clearInProgressMessage();
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
);
onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
}
clearInProgressMessage();
},
"Cancel",
undefined
);
} }
export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => { export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) { if (!newCellId) {
return; return;