mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 05:41:40 +00:00
Compare commits
2 Commits
offer_bug
...
ashleyst/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eed4df8c67 | ||
|
|
3418ce93ec |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +0,0 @@
|
||||
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -8,9 +8,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
jobs:
|
||||
codemetrics:
|
||||
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
|
||||
env:
|
||||
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:
|
||||
name: "End To End Emulator Tests"
|
||||
# Temporarily disabled. This test needs to be rewritten in playwright
|
||||
@@ -137,7 +140,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -148,18 +151,11 @@ jobs:
|
||||
- ./test/mongo/container.spec.ts
|
||||
- ./test/mongo/container32.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/tables/container.spec.ts
|
||||
steps:
|
||||
- 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
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
15
.github/workflows/cleanup.yml
vendored
15
.github/workflows/cleanup.yml
vendored
@@ -9,10 +9,6 @@ on:
|
||||
# Once every hour
|
||||
- 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
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
@@ -20,17 +16,10 @@ jobs:
|
||||
name: "Cleanup Test Database Accounts"
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
- 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
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
|
||||
@@ -124,9 +124,8 @@ export enum MongoBackendEndpointType {
|
||||
remote,
|
||||
}
|
||||
|
||||
export class BackendApi {
|
||||
public static readonly GenerateToken: string = "GenerateToken";
|
||||
public static readonly PortalSettings: string = "PortalSettings";
|
||||
export enum BackendApi {
|
||||
GenerateToken,
|
||||
}
|
||||
|
||||
export class PortalBackendEndpoints {
|
||||
|
||||
@@ -690,16 +690,9 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
|
||||
}
|
||||
|
||||
function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
];
|
||||
const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac];
|
||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (
|
||||
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development &&
|
||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||
) {
|
||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
NEW_BACKEND_APIS: [BackendApi.GenerateToken],
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
NEW_MONGO_APIS: [
|
||||
"resourcelist",
|
||||
|
||||
@@ -46,25 +46,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
]);
|
||||
}
|
||||
public componentDidUpdate(previous: EditorReactProps) {
|
||||
if (this.props.content !== previous.content) {
|
||||
this.editor?.setValue(this.props.content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +71,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||
// Hooking the model's onDidChangeContent event because of some event ordering issues.
|
||||
// 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(() => {
|
||||
queryEditorModel.onDidChangeContent(() => {
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.props.onContentChanged(queryEditorModel.getValue());
|
||||
});
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface MongoNotificationMessage {
|
||||
|
||||
export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): boolean => {
|
||||
const database: ViewModels.Database = collection.getDatabase();
|
||||
console.log(database?.isDatabaseShared(), collection.offer());
|
||||
return database?.isDatabaseShared() && !collection.offer();
|
||||
};
|
||||
|
||||
|
||||
@@ -98,11 +98,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
// Only call when expand has actually changed
|
||||
if (this.state.isExpanded !== prevState.isExpanded) {
|
||||
if (this.state.isExpanded) {
|
||||
console.log("IN HERE");
|
||||
this.props.node.onExpanded &&
|
||||
setTimeout(async () => {
|
||||
await this.props.node.onExpanded();
|
||||
}, TreeNodeComponent.callbackDelayMS);
|
||||
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent.callbackDelayMS);
|
||||
} else {
|
||||
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS);
|
||||
}
|
||||
|
||||
@@ -336,8 +336,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||
impacting the performance of transactional workloads."
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
@@ -487,4 +486,4 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
isButtonDisabled={false}
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
`;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Areas,
|
||||
BackendApi,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpStatusCodes,
|
||||
@@ -31,7 +30,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -82,11 +80,7 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
||||
};
|
||||
|
||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
|
||||
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
|
||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
@@ -20,6 +19,7 @@ import Explorer from "../Explorer";
|
||||
import * as TableConstants from "./Constants";
|
||||
import * as Entities from "./Entities";
|
||||
import * as TableEntityProcessor from "./TableEntityProcessor";
|
||||
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||
|
||||
export interface CassandraTableKeys {
|
||||
partitionKeys: CassandraTableKey[];
|
||||
@@ -732,16 +732,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
private useCassandraProxyEndpoint(api: string): boolean {
|
||||
const activeCassandraProxyEndpoints: string[] = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
];
|
||||
const activeCassandraProxyEndpoints: string[] = [CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Mpac];
|
||||
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||
if (
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
|
||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||
) {
|
||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
||||
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,8 +109,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
|
||||
: JSON.stringify(queryResults.documents, undefined, 4)
|
||||
: "";
|
||||
|
||||
console.log("QUERY RESULT: ", queryResultsString);
|
||||
|
||||
const onErrorDetailsClick = (): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
|
||||
@@ -357,7 +357,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
firstItemIndex,
|
||||
queryDocuments,
|
||||
);
|
||||
console.log("SETTING QUERY RESULTS", queryResults);
|
||||
this.setState({ queryResults, error: "" });
|
||||
} catch (error) {
|
||||
this.props.tabsBaseInstance.isExecutionError(true);
|
||||
@@ -497,16 +496,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
};
|
||||
|
||||
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({
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
});
|
||||
if (this.state.copilotActive) {
|
||||
this.props.copilotStore?.setQuery(newContent);
|
||||
}
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
if (newContent.length > 0) {
|
||||
this.executeQueryButton = {
|
||||
@@ -548,7 +544,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
public getEditorContent(): string {
|
||||
public setEditorContent(): string {
|
||||
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
|
||||
return this.state.queryCopilotGeneratedQuery;
|
||||
}
|
||||
@@ -605,7 +601,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
<div className="queryEditor" style={{ height: "100%" }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={this.getEditorContent()}
|
||||
content={this.setEditorContent()}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing Query"}
|
||||
|
||||
@@ -324,12 +324,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
|
||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||
if (
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||
(userContext.apiType === "Cassandra" &&
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||
ipRules?.length
|
||||
) {
|
||||
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
|
||||
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
||||
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
||||
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||
|
||||
@@ -264,16 +264,12 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
}
|
||||
|
||||
public async expandCollection(): Promise<void> {
|
||||
public expandCollection(): void {
|
||||
if (this.isCollectionExpanded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
|
||||
console.log("LOADED OFFERS", this.offer());
|
||||
this.isCollectionExpanded(true);
|
||||
|
||||
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
|
||||
description: "Collection node",
|
||||
|
||||
@@ -580,8 +576,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
public onSettingsClick = async (): Promise<void> => {
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
// const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
// throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
|
||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
description: "Settings node",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import { hasDatabaseSharedThroughput } from "Explorer/Controls/Settings/SettingsUtils";
|
||||
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
@@ -549,11 +548,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
id = database.isDatabaseShared() ? "sampleSettings" : "sampleScaleSettings";
|
||||
}
|
||||
|
||||
console.log("RUNNING HERE", database.id(), database.offer(), collection.id(), collection.offer());
|
||||
|
||||
children.push({
|
||||
id,
|
||||
label: hasDatabaseSharedThroughput(collection) || isServerlessAccount() ? "Settings" : "Scale & Settings",
|
||||
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
|
||||
onClick: collection.onSettingsClick.bind(collection),
|
||||
isSelected: () =>
|
||||
useSelectedNode
|
||||
@@ -601,7 +598,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||
onClick: () => {
|
||||
// Rewritten version of expandCollapseCollection
|
||||
console.log("CLICKED onClick");
|
||||
useSelectedNode.getState().setSelectedNode(collection);
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
refreshActiveTab(
|
||||
@@ -609,18 +605,13 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId,
|
||||
);
|
||||
},
|
||||
onExpanded: async () => {
|
||||
console.log("CLICKED onExpanded");
|
||||
await collection.expandCollection();
|
||||
onExpanded: () => {
|
||||
if (showScriptNodes) {
|
||||
collection.loadStoredProcedures();
|
||||
collection.loadUserDefinedFunctions();
|
||||
collection.loadTriggers();
|
||||
}
|
||||
},
|
||||
onCollapsed: () => {
|
||||
collection.collapseCollection();
|
||||
},
|
||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
|
||||
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Import this first, to ensure that the dev tools hook is copied before React is loaded.
|
||||
import "./ReactDevTools";
|
||||
|
||||
// CSS Dependencies
|
||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { usePortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
if (!usePortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
if (window.parent !== window) {
|
||||
try {
|
||||
(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).
|
||||
}
|
||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
}
|
||||
|
||||
@@ -145,22 +145,8 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
|
||||
//
|
||||
// Temporary function to determine if a portal backend API is supported by the
|
||||
// new backend in this environment.
|
||||
//
|
||||
// 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);
|
||||
export function usePortalBackendEndpoint(backendApi: BackendApi): boolean {
|
||||
const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development];
|
||||
const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false;
|
||||
return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as string);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { generateUniqueName } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(120000);
|
||||
|
||||
test("Cassandra keyspace and table CRUD", async () => {
|
||||
const keyspaceId = generateUniqueName("keyspace");
|
||||
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);
|
||||
|
||||
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");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(240000);
|
||||
|
||||
test("Graph CRUD", async () => {
|
||||
const databaseId = generateDatabaseNameWithTimestamp();
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
// Create new database and graph
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(240000);
|
||||
|
||||
test("Mongo CRUD", async () => {
|
||||
const databaseId = generateDatabaseNameWithTimestamp();
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
// Create new database and collection
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(240000);
|
||||
|
||||
test("Mongo CRUD", async () => {
|
||||
const databaseId = generateDatabaseNameWithTimestamp();
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
// Create new database and collection
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { getAzureCLICredentialsToken } from "../utils/shared";
|
||||
|
||||
test("Self Serve", async () => {
|
||||
// We can't retrieve AZ CLI credentials from the browser so we get them here.
|
||||
const token = await getAzureCLICredentialsToken();
|
||||
|
||||
await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`);
|
||||
await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
|
||||
const handle = await page.waitForSelector("iframe");
|
||||
const frame = await handle.contentFrame();
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { generateUniqueName } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
jest.setTimeout(120000);
|
||||
|
||||
test("SQL CRUD", async () => {
|
||||
const databaseId = generateUniqueName("db");
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
await explorer.click('[data-test="New Container"]');
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||
import { CosmosClient, PermissionMode } from "@azure/cosmos";
|
||||
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateUniqueName, getAzureCLICredentials } from "../utils/shared";
|
||||
import { generateUniqueName } from "../utils/shared";
|
||||
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";
|
||||
|
||||
test("Resource token", async () => {
|
||||
const credentials = await getAzureCLICredentials();
|
||||
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
|
||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
|
||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "expect-playwright";
|
||||
import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
|
||||
import { generateUniqueName } from "../utils/shared";
|
||||
import { waitForExplorer } from "../utils/waitForExplorer";
|
||||
|
||||
jest.setTimeout(120000);
|
||||
|
||||
test("Tables CRUD", async () => {
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
await page.waitForSelector('text="Querying databases"', { state: "detached" });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
import { ClientSecretCredential } from "@azure/identity";
|
||||
import "../../less/hostedexplorer.less";
|
||||
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
|
||||
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 selfServeType = urlSearchParams.get("selfServeType") || "example";
|
||||
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("Subcription: ", subscriptionId);
|
||||
console.log("Account Name: ", accountName);
|
||||
|
||||
const initTestExplorer = async (): Promise<void> => {
|
||||
const { token } = await credentials.getToken("https://management.azure.com//.default");
|
||||
updateUserContext({
|
||||
authorizationToken: `bearer ${token}`,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
|
||||
import crypto from "crypto";
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
|
||||
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
||||
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 thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
|
||||
@@ -16,7 +19,7 @@ function friendlyTime(date) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const credentials = await msRestNodeAuth.AzureCliCredentials.create();
|
||||
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
|
||||
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
||||
for (const account of accounts) {
|
||||
@@ -35,7 +38,7 @@ async function main() {
|
||||
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
|
||||
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
|
||||
resourceGroupName,
|
||||
account.name,
|
||||
account.name
|
||||
);
|
||||
for (const database of cassandraDatabases) {
|
||||
const timestamp = Number(database.resource._ts) * 1000;
|
||||
|
||||
Reference in New Issue
Block a user