Compare commits

..

2 Commits

Author SHA1 Message Date
Ashley Stanton-Nurse
eed4df8c67 remove pr template 2024-04-03 09:36:16 -07:00
Ashley Stanton-Nurse
3418ce93ec post the preview URL, with PR number, as a PR comment during the build 2024-04-03 09:29:46 -07:00
29 changed files with 101 additions and 210 deletions

View File

@@ -1 +0,0 @@
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)

View File

@@ -8,9 +8,6 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
permissions:
id-token: write
contents: read
jobs: jobs:
codemetrics: codemetrics:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -104,6 +101,12 @@ jobs:
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env: env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
- name: Post a comment with the preview URL
if: ${{ github.event_name == 'pull_request' }}
run: gh pr comment $PR_NUMBER --body "Build complete. You can now [preview this branch in the portal](https://cosmos-explorer-preview.azurewebsites.net/pull/$PR_NUMBER?feature.someFeatureFlagYouMightNeed=true)"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
endtoendemulator: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Emulator Tests"
# Temporarily disabled. This test needs to be rewritten in playwright # Temporarily disabled. This test needs to be rewritten in playwright
@@ -137,7 +140,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -148,18 +151,11 @@ jobs:
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts - ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:

View File

@@ -9,10 +9,6 @@ on:
# Once every hour # Once every hour
- cron: "0 15 * * *" - cron: "0 15 * * *"
permissions:
id-token: write
contents: read
# A workflow run is made up of one or more jobs that can run sequentially or in parallel # A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:
# This workflow contains a single job called "build" # This workflow contains a single job called "build"
@@ -20,17 +16,10 @@ jobs:
name: "Cleanup Test Database Accounts" name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:

View File

@@ -124,9 +124,8 @@ export enum MongoBackendEndpointType {
remote, remote,
} }
export class BackendApi { export enum BackendApi {
public static readonly GenerateToken: string = "GenerateToken"; GenerateToken,
public static readonly PortalSettings: string = "PortalSettings";
} }
export class PortalBackendEndpoints { export class PortalBackendEndpoints {

View File

@@ -690,16 +690,9 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
} }
function useMongoProxyEndpoint(api: string): boolean { function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [ const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac];
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if ( if (userContext.databaseAccount.properties.ipRules?.length > 0) {
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
} }

View File

@@ -99,6 +99,7 @@ let configContext: Readonly<ConfigContext> = {
JUNO_ENDPOINT: JunoEndpoints.Prod, JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
NEW_BACKEND_APIS: [BackendApi.GenerateToken],
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
NEW_MONGO_APIS: [ NEW_MONGO_APIS: [
"resourcelist", "resourcelist",

View File

@@ -46,25 +46,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
}, 100); }, 100);
} }
public componentDidUpdate() { public componentDidUpdate(previous: EditorReactProps) {
if (!this.editor) { if (this.props.content !== previous.content) {
return; this.editor?.setValue(this.props.content);
}
const existingContent = this.editor.getModel().getValue();
if (this.props.content !== existingContent) {
if (this.props.isReadOnly) {
this.editor.setValue(this.props.content);
} else {
this.editor.pushUndoStop();
this.editor.executeEdits("", [
{
range: this.editor.getModel().getFullModelRange(),
text: this.props.content,
},
]);
}
} }
} }
@@ -87,14 +71,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor; this.editor = editor;
const queryEditorModel = this.editor.getModel();
if (!this.props.isReadOnly && this.props.onContentChanged) { if (!this.props.isReadOnly && this.props.onContentChanged) {
// Hooking the model's onDidChangeContent event because of some event ordering issues. queryEditorModel.onDidChangeContent(() => {
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
// then there are some inconsistencies as to which event fires first.
// But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event.
// (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.)
// If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first.
this.editor.onDidChangeModelContent(() => {
const queryEditorModel = this.editor.getModel(); const queryEditorModel = this.editor.getModel();
this.props.onContentChanged(queryEditorModel.getValue()); this.props.onContentChanged(queryEditorModel.getValue());
}); });

View File

@@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
autoComplete="off" autoComplete="off"
styles={getTextFieldStyles()} styles={getTextFieldStyles()}
pattern="[^/?#\\-]*[^/?#- \\]" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'" title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new keyspace id" placeholder="Type a new keyspace id"
size={40} size={40}
value={newKeyspaceId} value={newKeyspaceId}
@@ -292,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
ariaLabel="addCollection-table Id Create table" ariaLabel="addCollection-table Id Create table"
autoComplete="off" autoComplete="off"
pattern="[^/?#\\-]*[^/?#- \\]" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'" title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Enter table Id" placeholder="Enter table Id"
size={20} size={20}
value={tableId} value={tableId}

View File

@@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsExecuting(true); setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities); const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try { try {
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
await tableEntityListViewModel.addEntityToCache(newEntity); await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled(); tableEntityListViewModel.redrawTableThrottled();

View File

@@ -218,7 +218,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A" generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
}\r\n`; }\r\n`;
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql; const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
const lastQuery = query ? `${query}\r\n` : ""; const lastQuery = generatedQuery && query ? `${query}\r\n` : "";
setQuery(`${lastQuery}${currentGeneratedQuery}`); setQuery(`${lastQuery}${currentGeneratedQuery}`);
setGeneratedQuery(generateSQLQueryResponse.sql); setGeneratedQuery(generateSQLQueryResponse.sql);
setGeneratedQueryComments(generateSQLQueryResponse.explanation); setGeneratedQueryComments(generateSQLQueryResponse.explanation);

View File

@@ -1,7 +1,6 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Areas, Areas,
BackendApi,
ConnectionStatusType, ConnectionStatusType,
ContainerStatusType, ContainerStatusType,
HttpStatusCodes, HttpStatusCodes,
@@ -31,7 +30,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@@ -82,11 +80,7 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
}; };
export const getCopilotEnabled = async (): Promise<boolean> => { export const getCopilotEnabled = async (): Promise<boolean> => {
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings) const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
? configContext.PORTAL_BACKEND_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token }; const headers = { [authorizationHeader.header]: authorizationHeader.token };

View File

@@ -3,7 +3,6 @@ import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
@@ -20,6 +19,7 @@ import Explorer from "../Explorer";
import * as TableConstants from "./Constants"; import * as TableConstants from "./Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import * as TableEntityProcessor from "./TableEntityProcessor"; import * as TableEntityProcessor from "./TableEntityProcessor";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
export interface CassandraTableKeys { export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[]; partitionKeys: CassandraTableKey[];
@@ -172,9 +172,8 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity); deferred.resolve(entity);
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); deferred.reject(error);
deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@@ -407,13 +406,12 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
handleError( handleError(
errorText, error,
"CreateKeyspaceCassandra", "CreateKeyspaceCassandra",
`Error while creating a keyspace with query ${createKeyspaceQuery}`, `Error while creating a keyspace with query ${createKeyspaceQuery}`,
); );
deferred.reject(errorText); deferred.reject(error);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@@ -446,13 +444,8 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
handleError( deferred.reject(error);
errorText,
"CreateTableCassandra",
`Error while creating a table with query ${createTableQuery}`,
);
deferred.reject(errorText);
}, },
) )
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
@@ -500,9 +493,8 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data); deferred.resolve(data);
}, },
(error: any) => { (error: any) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); deferred.reject(error);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@@ -541,9 +533,8 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data); deferred.resolve(data);
}, },
(error: any) => { (error: any) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); deferred.reject(error);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@@ -587,9 +578,8 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns); deferred.resolve(data.columns);
}, },
(error: any) => { (error: any) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); deferred.reject(error);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@@ -628,9 +618,8 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns); deferred.resolve(data.columns);
}, },
(error: any) => { (error: any) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); deferred.reject(error);
deferred.reject(errorText);
}, },
) )
.done(clearInProgressMessage); .done(clearInProgressMessage);
@@ -743,16 +732,9 @@ export class CassandraAPIDataClient extends TableDataClient {
} }
private useCassandraProxyEndpoint(api: string): boolean { private useCassandraProxyEndpoint(api: string): boolean {
const activeCassandraProxyEndpoints: string[] = [ const activeCassandraProxyEndpoints: string[] = [CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Mpac];
CassandraProxyEndpoints.Development,
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
];
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if ( if (userContext.databaseAccount.properties.ipRules?.length > 0) {
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
userContext.databaseAccount.properties.ipRules?.length > 0
) {
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
} }

View File

@@ -496,16 +496,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}; };
public onChangeContent(newContent: string): void { public onChangeContent(newContent: string): void {
// The copilot store's active query takes precedence over the local state,
// and we can't update both states in a single operation.
// So, we update the copilot store's state first, then update the local state.
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
this.setState({ this.setState({
sqlQueryEditorContent: newContent, sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "", queryCopilotGeneratedQuery: "",
}); });
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
if (this.isPreferredApiMongoDB) { if (this.isPreferredApiMongoDB) {
if (newContent.length > 0) { if (newContent.length > 0) {
this.executeQueryButton = { this.executeQueryButton = {
@@ -547,7 +544,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
public getEditorContent(): string { public setEditorContent(): string {
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) { if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
return this.state.queryCopilotGeneratedQuery; return this.state.queryCopilotGeneratedQuery;
} }
@@ -604,7 +601,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
<div className="queryEditor" style={{ height: "100%" }}> <div className="queryEditor" style={{ height: "100%" }}>
<EditorReact <EditorReact
language={"sql"} language={"sql"}
content={this.getEditorContent()} content={this.setEditorContent()}
isReadOnly={false} isReadOnly={false}
wordWrap={"on"} wordWrap={"on"}
ariaLabel={"Editing Query"} ariaLabel={"Editing Query"}

View File

@@ -324,12 +324,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if ( if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
(userContext.apiType === "Cassandra" &&
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
ipRules?.length
) {
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) => const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>

View File

@@ -1,6 +1,3 @@
// Import this first, to ensure that the dev tools hook is copied before React is loaded.
import "./ReactDevTools";
// CSS Dependencies // CSS Dependencies
import { initializeIcons, loadTheme } from "@fluentui/react"; import { initializeIcons, loadTheme } from "@fluentui/react";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel"; import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";

View File

@@ -1,6 +1,6 @@
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import { usePortalBackendEndpoint } from "Utils/EndpointUtils";
import * as React from "react"; import * as React from "react";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import ErrorImage from "../../../../images/error.svg"; import ErrorImage from "../../../../images/error.svg";
@@ -19,7 +19,7 @@ interface Props {
} }
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => { export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) { if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) {
return await fetchEncryptedToken_ToBeDeprecated(connectionString); return await fetchEncryptedToken_ToBeDeprecated(connectionString);
} }

View File

@@ -1,7 +1,3 @@
if (window.parent !== window) { if (window.parent !== window) {
try { (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__;
} catch {
// No-op. We can throw here if the parent is not the same origin (such as in the Azure portal).
}
} }

View File

@@ -145,22 +145,8 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
export const allowedNotebookServerUrls: ReadonlyArray<string> = []; export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
// export function usePortalBackendEndpoint(backendApi: BackendApi): boolean {
// Temporary function to determine if a portal backend API is supported by the const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development];
// new backend in this environment. const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false;
// return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as string);
// TODO: Remove this function once new backend migration is completed for all environments.
//
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
// This maps backend APIs to the environments supported by the new backend.
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
[BackendApi.GenerateToken]: [PortalBackendEndpoints.Development],
[BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
};
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
return false;
}
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
} }

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Cassandra keyspace and table CRUD", async () => { test("Cassandra keyspace and table CRUD", async () => {
const keyspaceId = generateUniqueName("keyspace"); const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table"); const tableId = generateUniqueName("table");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000); page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner");
await page.waitForSelector("iframe"); await page.waitForSelector("iframe");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Graph CRUD", async () => { test("Graph CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000); page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Create new database and graph // Create new database and graph

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000); page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Create new database and collection // Create new database and collection

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000); page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
// Create new database and collection // Create new database and collection

View File

@@ -1,10 +1,5 @@
import { getAzureCLICredentialsToken } from "../utils/shared";
test("Self Serve", async () => { test("Self Serve", async () => {
// We can't retrieve AZ CLI credentials from the browser so we get them here. await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
const token = await getAzureCLICredentialsToken();
await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`);
const handle = await page.waitForSelector("iframe"); const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame(); const frame = await handle.contentFrame();

View File

@@ -1,18 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("SQL CRUD", async () => { test("SQL CRUD", async () => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000); page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
await explorer.click('[data-test="New Container"]'); await explorer.click('[data-test="New Container"]');

View File

@@ -1,15 +1,19 @@
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { CosmosClient, PermissionMode } from "@azure/cosmos"; import { CosmosClient, PermissionMode } from "@azure/cosmos";
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName, getAzureCLICredentials } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
jest.setTimeout(120000); jest.setTimeout(120000);
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? ""; const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4";
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners"; const resourceGroupName = "runners";
test("Resource token", async () => { test("Resource token", async () => {
const credentials = await getAzureCLICredentials(); const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us"); const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us"); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");

View File

@@ -1,17 +1,15 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Tables CRUD", async () => { test("Tables CRUD", async () => {
const tableId = generateUniqueName("table"); const tableId = generateUniqueName("table");
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
page.setDefaultTimeout(50000); page.setDefaultTimeout(50000);
await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();
await page.waitForSelector('text="Querying databases"', { state: "detached" }); await page.waitForSelector('text="Querying databases"', { state: "detached" });

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { ClientSecretCredential } from "@azure/identity";
import "../../less/hostedexplorer.less"; import "../../less/hostedexplorer.less";
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
import { updateUserContext } from "../../src/UserContext"; import { updateUserContext } from "../../src/UserContext";
@@ -10,13 +11,29 @@ const urlSearchParams = new URLSearchParams(window.location.search);
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us"; const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
const selfServeType = urlSearchParams.get("selfServeType") || "example"; const selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache"; const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
const token = urlSearchParams.get("token");
if (!process.env.AZURE_CLIENT_SECRET) {
throw new Error(
"process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server",
);
}
// Azure SDK clients accept the credential as a parameter
const credentials = new ClientSecretCredential(
process.env.AZURE_TENANT_ID,
process.env.AZURE_CLIENT_ID,
process.env.AZURE_CLIENT_SECRET,
{
authorityHost: "https://localhost:1234",
},
);
console.log("Resource Group:", resourceGroup); console.log("Resource Group:", resourceGroup);
console.log("Subcription: ", subscriptionId); console.log("Subcription: ", subscriptionId);
console.log("Account Name: ", accountName); console.log("Account Name: ", accountName);
const initTestExplorer = async (): Promise<void> => { const initTestExplorer = async (): Promise<void> => {
const { token } = await credentials.getToken("https://management.azure.com//.default");
updateUserContext({ updateUserContext({
authorizationToken: `bearer ${token}`, authorizationToken: `bearer ${token}`,
}); });
@@ -35,9 +52,6 @@ const initTestExplorer = async (): Promise<void> => {
dnsSuffix: "documents.azure.com", dnsSuffix: "documents.azure.com",
serverId: "prod1", serverId: "prod1",
extensionEndpoint: "/proxy", extensionEndpoint: "/proxy",
portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com",
cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com",
subscriptionType: 3, subscriptionType: 3,
quotaId: "Internal_2014-09-01", quotaId: "Internal_2014-09-01",
isTryCosmosDBSubscription: false, isTryCosmosDBSubscription: false,

View File

@@ -1,4 +1,3 @@
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
import crypto from "crypto"; import crypto from "crypto";
export function generateUniqueName(baseName = "", length = 4): string { export function generateUniqueName(baseName = "", length = 4): string {
@@ -8,13 +7,3 @@ export function generateUniqueName(baseName = "", length = 4): string {
export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string { export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
} }
export async function getAzureCLICredentials(): Promise<AzureCliCredentials> {
return await AzureCliCredentials.create();
}
export async function getAzureCLICredentialsToken(): Promise<string> {
const credentials = await getAzureCLICredentials();
const token = (await credentials.getToken()).accessToken;
return token;
}

View File

@@ -2,7 +2,10 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
const ms = require("ms"); const ms = require("ms");
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners"; const resourceGroupName = "runners";
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
@@ -16,7 +19,7 @@ function friendlyTime(date) {
} }
async function main() { async function main() {
const credentials = await msRestNodeAuth.AzureCliCredentials.create(); const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const client = new CosmosDBManagementClient(credentials, subscriptionId); const client = new CosmosDBManagementClient(credentials, subscriptionId);
const accounts = await client.databaseAccounts.list(resourceGroupName); const accounts = await client.databaseAccounts.list(resourceGroupName);
for (const account of accounts) { for (const account of accounts) {
@@ -35,7 +38,7 @@ async function main() {
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) { } else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces( const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
resourceGroupName, resourceGroupName,
account.name, account.name
); );
for (const database of cassandraDatabases) { for (const database of cassandraDatabases) {
const timestamp = Number(database.resource._ts) * 1000; const timestamp = Number(database.resource._ts) * 1000;