mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 11:11:23 +00:00
Compare commits
7 Commits
users/aisa
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40a530d572 | ||
|
|
15d111e3db | ||
|
|
1726e4df51 | ||
|
|
bc68b4dbf9 | ||
|
|
15e35eaa82 | ||
|
|
f69cd4c495 | ||
|
|
661f6f4bfb |
1456
package-lock.json
generated
1456
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
"@fluentui/react": "8.14.3",
|
||||
"@fluentui/react-components": "9.32.1",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@jupyterlab/terminal": "4.0.8",
|
||||
"@microsoft/applicationinsights-web": "2.6.1",
|
||||
"@nteract/commutable": "7.4.2",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
|
||||
@@ -35,14 +35,21 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
}
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
responses.forEach((response) => {
|
||||
collections.push(response.resource as DataModels.Collection);
|
||||
});
|
||||
try {
|
||||
const responses = await Promise.all(promises);
|
||||
responses.forEach((response) => {
|
||||
collections.push(response.resource as DataModels.Collection);
|
||||
});
|
||||
|
||||
// Sort collections by id before returning
|
||||
collections.sort((a, b) => a.id.localeCompare(b.id));
|
||||
return collections;
|
||||
// Sort collections by id before returning
|
||||
collections.sort((a, b) => a.id.localeCompare(b.id));
|
||||
return collections;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -22,6 +22,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
for (const collectionResourceId in tokensData.resourceTokens) {
|
||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||
const resourceIdObj = collectionResourceId.split("/");
|
||||
|
||||
if (resourceIdObj.length !== 4) {
|
||||
handleError(`Resource key not recognized: ${resourceIdObj}`, "ReadDatabases", `Error while querying databases`);
|
||||
clearMessage();
|
||||
return [];
|
||||
}
|
||||
|
||||
const databaseId = resourceIdObj[1];
|
||||
|
||||
databaseIdsSet.add(databaseId);
|
||||
@@ -37,6 +44,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
id: databaseId,
|
||||
collections: [],
|
||||
}));
|
||||
clearMessage();
|
||||
return databases;
|
||||
}
|
||||
|
||||
|
||||
13
src/Contracts/AzureResourceGraph.ts
Normal file
13
src/Contracts/AzureResourceGraph.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface QueryRequestOptions {
|
||||
$skipToken?: string;
|
||||
$top?: number;
|
||||
subscriptions: string[];
|
||||
}
|
||||
|
||||
export interface QueryResponse {
|
||||
$skipToken: string;
|
||||
count: number;
|
||||
data: any;
|
||||
resultTruncated: boolean;
|
||||
totalRecords: number;
|
||||
}
|
||||
@@ -88,13 +88,13 @@ export interface GenerateTokenResponse {
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
uniqueDisplayName: string;
|
||||
uniqueDisplayName?: string;
|
||||
displayName: string;
|
||||
subscriptionId: string;
|
||||
tenantId: string;
|
||||
tenantId?: string;
|
||||
state: string;
|
||||
subscriptionPolicies: SubscriptionPolicies;
|
||||
authorizationSource: string;
|
||||
subscriptionPolicies?: SubscriptionPolicies;
|
||||
authorizationSource?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptionPolicies {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const newDbAndCollectionCommand = `use quickstartDB
|
||||
db.createCollection('sampleCollection')`;
|
||||
db.createCollection('sampleCollection')
|
||||
`;
|
||||
|
||||
export const newDbAndCollectionCommandForDisplay = `use quickstartDB // Create new database named 'quickstartDB' or switch to it if it already exists
|
||||
|
||||
@@ -16,19 +17,25 @@ export const loadDataCommand = `db.sampleCollection.insertMany([
|
||||
{title: "War and Peace", author: "Leo Tolstoy", pages: 1392},
|
||||
{title: "The Odyssey", author: "Homer", pages: 374},
|
||||
{title: "Ulysses", author: "James Joyce", pages: 730}
|
||||
])`;
|
||||
])
|
||||
`;
|
||||
|
||||
export const queriesCommand = `db.sampleCollection.find({author: "George Orwell"})
|
||||
export const findOrwellCommand = `db.sampleCollection.find({author: "George Orwell"})
|
||||
`;
|
||||
|
||||
export const findOrwellCommandForDisplay = `// Query to find all books written by "George Orwell"
|
||||
db.sampleCollection.find({author: "George Orwell"})`;
|
||||
|
||||
export const findByPagesCommand = `db.sampleCollection.find({pages: {$gt: 500}})
|
||||
`;
|
||||
|
||||
export const findByPagesCommandForDisplay = `// Query to find all books with more than 500 pages
|
||||
db.sampleCollection.find({pages: {$gt: 500}})
|
||||
`;
|
||||
|
||||
db.sampleCollection.find({}).sort({pages: 1})`;
|
||||
export const findAndSortCommand = `db.sampleCollection.find({}).sort({pages: 1})
|
||||
`;
|
||||
|
||||
export const queriesCommandForDisplay = `// Query to find all books written by "George Orwell"
|
||||
db.sampleCollection.find({author: "George Orwell"})
|
||||
|
||||
// Query to find all books with more than 500 pages
|
||||
db.sampleCollection.find({pages: {$gt: 500}})
|
||||
|
||||
// Query to find all books and sort them by the number of pages in ascending order
|
||||
db.sampleCollection.find({}).sort({pages: 1})`;
|
||||
export const findAndSortCommandForDisplay = `// Query to find all books and sort them by the number of pages in ascending order
|
||||
db.sampleCollection.find({}).sort({pages: 1})
|
||||
`;
|
||||
|
||||
@@ -11,11 +11,15 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
|
||||
import {
|
||||
findAndSortCommand,
|
||||
findAndSortCommandForDisplay,
|
||||
findByPagesCommand,
|
||||
findByPagesCommandForDisplay,
|
||||
findOrwellCommand,
|
||||
findOrwellCommandForDisplay,
|
||||
loadDataCommand,
|
||||
newDbAndCollectionCommand,
|
||||
newDbAndCollectionCommandForDisplay,
|
||||
queriesCommand,
|
||||
queriesCommandForDisplay,
|
||||
} from "Explorer/Quickstart/VCoreMongoQuickstartCommands";
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import React, { useState } from "react";
|
||||
@@ -190,17 +194,17 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 110 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
|
||||
onClick={() => useTerminal.getState().sendMessage(findOrwellCommand)}
|
||||
>
|
||||
Try query
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="queriesCommand"
|
||||
id="findOrwellCommand"
|
||||
multiline
|
||||
rows={5}
|
||||
rows={2}
|
||||
readOnly
|
||||
defaultValue={queriesCommandForDisplay}
|
||||
defaultValue={findOrwellCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
@@ -214,7 +218,65 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
iconProps={{
|
||||
iconName: "Copy",
|
||||
}}
|
||||
onClick={() => onCopyBtnClicked("#queriesCommand")}
|
||||
onClick={() => onCopyBtnClicked("#findOrwellCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 32, width: 110 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(findByPagesCommand)}
|
||||
>
|
||||
Try query
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="findByPagesCommand"
|
||||
multiline
|
||||
rows={2}
|
||||
readOnly
|
||||
defaultValue={findByPagesCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: "Copy",
|
||||
}}
|
||||
onClick={() => onCopyBtnClicked("#findByPagesCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 32, width: 110 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(findAndSortCommand)}
|
||||
>
|
||||
Try query
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="findAndSortCommand"
|
||||
multiline
|
||||
rows={2}
|
||||
readOnly
|
||||
defaultValue={findAndSortCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: "Copy",
|
||||
}}
|
||||
onClick={() => onCopyBtnClicked("#findAndSortCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -236,7 +298,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
hosted in the cloud, to Azure Cosmos DB for MongoDB vCore.
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://learn.microsoft.com/azure-data-studio/extensions/azure-cosmos-db-mongodb-extension"
|
||||
href="https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/migration-options"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
|
||||
@@ -7,9 +7,6 @@ import "../less/hostedexplorer.less";
|
||||
import { AuthType } from "./AuthType";
|
||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||
import "./Explorer/Menus/NavBar/MeControlComponent.less";
|
||||
import { useAADAuth } from "./hooks/useAADAuth";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
||||
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
||||
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
|
||||
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
|
||||
@@ -20,6 +17,9 @@ import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
||||
import "./Platform/Hosted/ConnectScreen.less";
|
||||
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
|
||||
import "./Shared/appInsights";
|
||||
import { useAADAuth } from "./hooks/useAADAuth";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
setErrorMessage("");
|
||||
|
||||
if (await isAccountRestrictedForConnectionStringLogin(connectionString)) {
|
||||
setErrorMessage("This account has been blocked from connection-string login.");
|
||||
setErrorMessage(
|
||||
"This account has been blocked from connection-string login. Please go to cosmos.azure.com/aad for AAD based login.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
}
|
||||
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails {
|
||||
bottom: 24px;
|
||||
width: 145px;
|
||||
width: 165px;
|
||||
visibility: hidden;
|
||||
background-color: #393939;
|
||||
color: #ffffff;
|
||||
|
||||
@@ -14,6 +14,7 @@ export type Features = {
|
||||
readonly enableTtl: boolean;
|
||||
readonly executeSproc: boolean;
|
||||
readonly enableAadDataPlane: boolean;
|
||||
readonly enableResourceGraph: boolean;
|
||||
readonly enableKoResourceTree: boolean;
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
@@ -73,6 +74,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
canExceedMaximumValue: "true" === get("canexceedmaximumvalue"),
|
||||
cosmosdb: "true" === get("cosmosdb"),
|
||||
enableAadDataPlane: "true" === get("enableaaddataplane"),
|
||||
enableResourceGraph: "true" === get("enableresourcegraph"),
|
||||
enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"),
|
||||
enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"),
|
||||
enableKOPanel: "true" === get("enablekopanel"),
|
||||
|
||||
@@ -10,9 +10,13 @@ import { userContext } from "UserContext";
|
||||
export class JupyterLabAppFactory {
|
||||
private isShellStarted: boolean | undefined;
|
||||
private checkShellStarted: ((content: string | undefined) => void) | undefined;
|
||||
private onShellExited: () => void;
|
||||
private onShellExited: (restartShell: boolean) => void;
|
||||
private restartShell: boolean;
|
||||
|
||||
private isShellExited(content: string | undefined) {
|
||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
||||
this.restartShell = true;
|
||||
}
|
||||
return content?.includes("cosmosuser@");
|
||||
}
|
||||
|
||||
@@ -32,10 +36,11 @@ export class JupyterLabAppFactory {
|
||||
this.isShellStarted = content?.includes("Enter password");
|
||||
}
|
||||
|
||||
constructor(closeTab: () => void) {
|
||||
constructor(closeTab: (restartShell: boolean) => void) {
|
||||
this.onShellExited = closeTab;
|
||||
this.isShellStarted = false;
|
||||
this.checkShellStarted = undefined;
|
||||
this.restartShell = false;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
@@ -69,7 +74,7 @@ export class JupyterLabAppFactory {
|
||||
if (!this.isShellStarted) {
|
||||
this.checkShellStarted(content);
|
||||
} else if (this.isShellExited(content)) {
|
||||
this.onShellExited();
|
||||
this.onShellExited(this.restartShell);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
@@ -11,6 +11,8 @@ import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
||||
import { TerminalProps } from "./TerminalProps";
|
||||
import "./index.css";
|
||||
|
||||
let session: ITerminalConnection | undefined;
|
||||
|
||||
const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => {
|
||||
let body: BodyInit | undefined;
|
||||
let headers: HeadersInit | undefined;
|
||||
@@ -49,7 +51,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings
|
||||
return ServerConnection.makeSettings(options);
|
||||
};
|
||||
|
||||
const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection | undefined> => {
|
||||
const initTerminal = async (props: TerminalProps): Promise<void> => {
|
||||
// Initialize userContext (only properties which are needed by TelemetryProcessor)
|
||||
updateUserContext({
|
||||
subscriptionId: props.subscriptionId,
|
||||
@@ -59,28 +61,37 @@ const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection |
|
||||
});
|
||||
|
||||
const serverSettings = createServerSettings(props);
|
||||
|
||||
createTerminalApp(props, serverSettings);
|
||||
};
|
||||
|
||||
const createTerminalApp = async (props: TerminalProps, serverSettings: ServerConnection.ISettings) => {
|
||||
const data = { baseUrl: serverSettings.baseUrl };
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
const session = await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
session = await new JupyterLabAppFactory((restartShell: boolean) =>
|
||||
closeTab(props, serverSettings, restartShell),
|
||||
).createTerminalApp(serverSettings);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
return session;
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
return undefined;
|
||||
session = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (tabId: string): void => {
|
||||
window.parent.postMessage(
|
||||
{ type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" },
|
||||
window.document.referrer,
|
||||
);
|
||||
const closeTab = (props: TerminalProps, serverSettings: ServerConnection.ISettings, restartShell: boolean): void => {
|
||||
if (restartShell) {
|
||||
createTerminalApp(props, serverSettings);
|
||||
} else {
|
||||
window.parent.postMessage(
|
||||
{ type: MessageTypes.CloseTab, data: { tabId: props.tabId }, signature: "pcIframe" },
|
||||
window.document.referrer,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
let session: ITerminalConnection | undefined;
|
||||
postRobot.on(
|
||||
"props",
|
||||
{
|
||||
@@ -91,7 +102,7 @@ const main = async (): Promise<void> => {
|
||||
// Typescript definition for event is wrong. So read props by casting to <any>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const props = (event as any).data as TerminalProps;
|
||||
session = await initTerminal(props);
|
||||
await initTerminal(props);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
|
||||
import useSWR from "swr";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface AccountListResult {
|
||||
nextLink: string;
|
||||
@@ -30,10 +33,56 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
|
||||
return accounts.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export async function fetchDatabaseAccountsFromGraph(
|
||||
subscriptionId: string,
|
||||
accessToken: string,
|
||||
): Promise<DatabaseAccount[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
headers.append(HttpHeaders.contentType, "application/json");
|
||||
const databaseAccountsQuery = "resources | where type =~ 'microsoft.documentdb/databaseaccounts'";
|
||||
const apiVersion = "2021-03-01";
|
||||
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
|
||||
|
||||
const databaseAccounts: DatabaseAccount[] = [];
|
||||
let skipToken: string;
|
||||
do {
|
||||
const body = {
|
||||
query: databaseAccountsQuery,
|
||||
subscriptions: [subscriptionId],
|
||||
...(skipToken && {
|
||||
options: {
|
||||
$skipToken: skipToken,
|
||||
} as QueryRequestOptions,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch(managementResourceGraphAPIURL, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
const queryResponse: QueryResponse = (await response.json()) as QueryResponse;
|
||||
skipToken = queryResponse.$skipToken;
|
||||
queryResponse.data?.map((databaseAccount: any) => {
|
||||
databaseAccounts.push(databaseAccount as DatabaseAccount);
|
||||
});
|
||||
} while (skipToken);
|
||||
|
||||
return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined {
|
||||
const { data } = useSWR(
|
||||
() => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined),
|
||||
(_, subscriptionId, armToken) => fetchDatabaseAccounts(subscriptionId, armToken),
|
||||
(_, subscriptionId, armToken) => fetchDatabaseAccountsFromGraph(subscriptionId, armToken),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
|
||||
import useSWR from "swr";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { Subscription } from "../Contracts/DataModels";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface SubscriptionListResult {
|
||||
nextLink: string;
|
||||
@@ -32,10 +35,58 @@ export async function fetchSubscriptions(accessToken: string): Promise<Subscript
|
||||
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
}
|
||||
|
||||
export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<Subscription[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
headers.append(HttpHeaders.contentType, "application/json");
|
||||
const subscriptionsQuery =
|
||||
"resources | where type == 'microsoft.documentdb/databaseaccounts' | join kind=inner ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project subscriptionId, subscriptionName = name, subscriptionState = tostring(parse_json(properties).state) ) on subscriptionId | summarize by subscriptionId, subscriptionName, subscriptionState";
|
||||
const apiVersion = "2021-03-01";
|
||||
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
|
||||
|
||||
const subscriptions: Subscription[] = [];
|
||||
let skipToken: string;
|
||||
do {
|
||||
const body = {
|
||||
query: subscriptionsQuery,
|
||||
...(skipToken && {
|
||||
options: {
|
||||
$skipToken: skipToken,
|
||||
} as QueryRequestOptions,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch(managementResourceGraphAPIURL, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
const queryResponse: QueryResponse = (await response.json()) as QueryResponse;
|
||||
skipToken = queryResponse.$skipToken;
|
||||
|
||||
queryResponse.data?.map((subscription: any) => {
|
||||
subscriptions.push({
|
||||
displayName: subscription.subscriptionName,
|
||||
subscriptionId: subscription.subscriptionId,
|
||||
state: subscription.subscriptionState,
|
||||
} as Subscription);
|
||||
});
|
||||
} while (skipToken);
|
||||
|
||||
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
}
|
||||
|
||||
export function useSubscriptions(armToken: string): Subscription[] | undefined {
|
||||
const { data } = useSWR(
|
||||
() => (armToken ? ["subscriptions", armToken] : undefined),
|
||||
(_, armToken) => fetchSubscriptions(armToken),
|
||||
(_, armToken) => fetchSubscriptionsFromGraph(armToken),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user