mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-02-04 01:24:31 +00:00
Compare commits
52 Commits
release/ho
...
users/sind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a1ca321d | ||
|
|
d12f10afd8 | ||
|
|
80b926214b | ||
|
|
070b7a4ca7 | ||
|
|
d61ff5dcb5 | ||
|
|
d42eebaa5a | ||
|
|
b456e53b2f | ||
|
|
de5ba041e9 | ||
|
|
ae7184f7ea | ||
|
|
3a6769280b | ||
|
|
4768ba3642 | ||
|
|
bd564c665b | ||
|
|
7e5c6420ad | ||
|
|
89a3a040d8 | ||
|
|
ec3afa0526 | ||
|
|
4176a8a9a9 | ||
|
|
bc8094f44f | ||
|
|
d7825f4f78 | ||
|
|
e51c28c634 | ||
|
|
2b84af60f4 | ||
|
|
29a1a819c3 | ||
|
|
5a16eec29d | ||
|
|
2b11e0e52b | ||
|
|
89374a16ba | ||
|
|
dc289ece75 | ||
|
|
1ddd372c6d | ||
|
|
2740657b4a | ||
|
|
8c888a751c | ||
|
|
8140f0edb1 | ||
|
|
ab5239df09 | ||
|
|
3e48393fbb | ||
|
|
0079a9147f | ||
|
|
912688dc14 | ||
|
|
8849526fab | ||
|
|
24af64a66d | ||
|
|
be871737ad | ||
|
|
4d8bb5c3ea | ||
|
|
10a8505b9a | ||
|
|
ef7c2fe2f7 | ||
|
|
4c7aca95e1 | ||
|
|
2243ad895a | ||
|
|
b2d5f91fe1 | ||
|
|
a712193477 | ||
|
|
5ee411693c | ||
|
|
16c7b2567b | ||
|
|
78d9a0cd8d | ||
|
|
c6ad538559 | ||
|
|
2bc09a6efe | ||
|
|
d3a3033b25 | ||
|
|
6bdc714e11 | ||
|
|
5042f28229 | ||
|
|
e1430fd06f |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
|||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
- run: cp -r ./Contracts ./dist/contracts
|
- run: cp -r ./Contracts ./dist/contracts
|
||||||
- run: cp -r ./configs ./dist/configs
|
- run: cp -r ./configs ./dist/configs
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -117,14 +117,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/prod.json config.json
|
- run: cp ./configs/prod.json config.json
|
||||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: packages
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
@@ -141,7 +141,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/mpac.json config.json
|
- run: cp ./configs/mpac.json config.json
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: packages
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
|
|||||||
@@ -1906,20 +1906,13 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
|
height: 32px;
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: -0.5px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
// Override the bootstrap defaults here to align with our layout constants.
|
|
||||||
margin-bottom: 0px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { AuthType } from "../AuthType";
|
|||||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
|
import { runCommand } from "hooks/useDatabaseAccounts";
|
||||||
|
|
||||||
const _global = typeof self === "undefined" ? window : self;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||||
|
console.log("AAD Token ", userContext.aadToken);
|
||||||
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
||||||
return authorizationToken;
|
return authorizationToken;
|
||||||
}
|
}
|
||||||
@@ -209,10 +211,14 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
|
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wrappedTokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
|
return await runCommand(tokenProvider, requestInfo);
|
||||||
|
};
|
||||||
|
|
||||||
const options: Cosmos.CosmosClientOptions = {
|
const options: Cosmos.CosmosClientOptions = {
|
||||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||||
key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey,
|
key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey,
|
||||||
tokenProvider,
|
tokenProvider: wrappedTokenProvider,
|
||||||
userAgentSuffix: "Azure Portal",
|
userAgentSuffix: "Azure Portal",
|
||||||
defaultHeaders: _defaultHeaders,
|
defaultHeaders: _defaultHeaders,
|
||||||
connectionPolicy: {
|
connectionPolicy: {
|
||||||
|
|||||||
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
// This component is used to create a dropdown list of options for the user to select from.
|
||||||
|
// The options are displayed in a dropdown list when the user clicks on the input field.
|
||||||
|
// The user can then select an option from the list. The selected option is then displayed in the input field.
|
||||||
|
|
||||||
|
import { getTheme } from "@fluentui/react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Link,
|
||||||
|
makeStyles,
|
||||||
|
Popover,
|
||||||
|
PopoverProps,
|
||||||
|
PopoverSurface,
|
||||||
|
PositioningImperativeRef,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { ArrowDownRegular, DismissRegular } from "@fluentui/react-icons";
|
||||||
|
import { NormalizedEventKey } from "Common/Constants";
|
||||||
|
import { tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import React, { FC, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
container: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flexGrow: 1,
|
||||||
|
paddingRight: 0,
|
||||||
|
outline: "none",
|
||||||
|
"& input:focus": {
|
||||||
|
outline: "none", // Undo body :focus dashed outline
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputButton: {
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
dropdownHeader: {
|
||||||
|
width: "100%",
|
||||||
|
fontSize: tokens.fontSizeBase300,
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: `${tokens.spacingVerticalM} 0 0 ${tokens.spacingVerticalM}`,
|
||||||
|
},
|
||||||
|
dropdownStack: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: tokens.spacingVerticalS,
|
||||||
|
marginTop: tokens.spacingVerticalS,
|
||||||
|
marginBottom: "1px",
|
||||||
|
},
|
||||||
|
dropdownOption: {
|
||||||
|
fontSize: tokens.fontSizeBase300,
|
||||||
|
fontWeight: 400,
|
||||||
|
justifyContent: "left",
|
||||||
|
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
border: 0,
|
||||||
|
":hover": {
|
||||||
|
outline: `1px dashed ${tokens.colorNeutralForeground1Hover}`,
|
||||||
|
backgroundColor: tokens.colorNeutralBackground2Hover,
|
||||||
|
color: tokens.colorNeutralForeground1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomSection: {
|
||||||
|
fontSize: tokens.fontSizeBase300,
|
||||||
|
fontWeight: 400,
|
||||||
|
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface InputDatalistDropdownOptionSection {
|
||||||
|
label: string;
|
||||||
|
options: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputDataListProps {
|
||||||
|
dropdownOptions: InputDatalistDropdownOptionSection[];
|
||||||
|
placeholder?: string;
|
||||||
|
title?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
autofocus?: boolean; // true: acquire focus on first render
|
||||||
|
bottomLink?: {
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputDataList: FC<InputDataListProps> = ({
|
||||||
|
dropdownOptions,
|
||||||
|
placeholder,
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
autofocus,
|
||||||
|
bottomLink,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const [showDropdown, setShowDropdown] = React.useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const positioningRef = React.useRef<PositioningImperativeRef>(null);
|
||||||
|
const [isInputFocused, setIsInputFocused] = React.useState(autofocus);
|
||||||
|
const [autofocusFirstDropdownItem, setAutofocusFirstDropdownItem] = React.useState(false);
|
||||||
|
|
||||||
|
const theme = getTheme();
|
||||||
|
const itemRefs = useRef([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
positioningRef.current?.setTarget(inputRef.current);
|
||||||
|
}
|
||||||
|
}, [inputRef, positioningRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInputFocused) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isInputFocused]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autofocusFirstDropdownItem && showDropdown) {
|
||||||
|
// Autofocus on first item if input isn't focused
|
||||||
|
itemRefs.current[0]?.focus();
|
||||||
|
setAutofocusFirstDropdownItem(false);
|
||||||
|
}
|
||||||
|
}, [autofocusFirstDropdownItem, showDropdown]);
|
||||||
|
|
||||||
|
const handleOpenChange: PopoverProps["onOpenChange"] = (e, data) => {
|
||||||
|
if (isInputFocused && !data.open) {
|
||||||
|
// Don't close if input is focused and we're opening the dropdown (which will steal the focus)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowDropdown(data.open || false);
|
||||||
|
if (data.open) {
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === NormalizedEventKey.Escape) {
|
||||||
|
setShowDropdown(false);
|
||||||
|
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||||
|
setShowDropdown(true);
|
||||||
|
setAutofocusFirstDropdownItem(true);
|
||||||
|
}
|
||||||
|
onKeyDown(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownDropdownItemKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
if (e.key === NormalizedEventKey.Enter) {
|
||||||
|
e.currentTarget.click();
|
||||||
|
} else if (e.key === NormalizedEventKey.Escape) {
|
||||||
|
setShowDropdown(false);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||||
|
if (index + 1 < itemRefs.current.length) {
|
||||||
|
itemRefs.current[index + 1].focus();
|
||||||
|
} else {
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}
|
||||||
|
} else if (e.key === NormalizedEventKey.UpArrow) {
|
||||||
|
if (index - 1 >= 0) {
|
||||||
|
itemRefs.current[index - 1].focus();
|
||||||
|
} else {
|
||||||
|
// Last item, focus back to input
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flatten dropdownOptions to better manage refs and focus
|
||||||
|
let flatIndex = 0;
|
||||||
|
const indexMap = new Map<string, number>();
|
||||||
|
for (let sectionIndex = 0; sectionIndex < dropdownOptions.length; sectionIndex++) {
|
||||||
|
const section = dropdownOptions[sectionIndex];
|
||||||
|
for (let optionIndex = 0; optionIndex < section.options.length; optionIndex++) {
|
||||||
|
indexMap.set(`${sectionIndex}-${optionIndex}`, flatIndex);
|
||||||
|
flatIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
id="filterInput"
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
autoComplete="off"
|
||||||
|
className={`filterInput ${styles.input}`}
|
||||||
|
title={title}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
// Don't show dropdown if there is already a value in the input field (when user is typing)
|
||||||
|
setShowDropdown(!(newValue.length > 0));
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
// Don't show dropdown if there is already a value in the input field
|
||||||
|
// or isInputFocused is undefined which means component is mounting
|
||||||
|
setShowDropdown(!(value.length > 0) && isInputFocused !== undefined);
|
||||||
|
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setIsInputFocused(false);
|
||||||
|
}}
|
||||||
|
contentAfter={
|
||||||
|
value.length > 0 ? (
|
||||||
|
<Button
|
||||||
|
aria-label="Clear filter"
|
||||||
|
className={styles.inputButton}
|
||||||
|
size="small"
|
||||||
|
icon={<DismissRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
onChange("");
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
aria-label="Open dropdown"
|
||||||
|
className={styles.inputButton}
|
||||||
|
size="small"
|
||||||
|
icon={<ArrowDownRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
setShowDropdown(true);
|
||||||
|
setAutofocusFirstDropdownItem(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Popover
|
||||||
|
inline
|
||||||
|
unstable_disableAutoFocus
|
||||||
|
// trapFocus
|
||||||
|
open={showDropdown}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
positioning={{ positioningRef, position: "below", align: "start", offset: 4 }}
|
||||||
|
>
|
||||||
|
<PopoverSurface className={styles.container}>
|
||||||
|
{dropdownOptions.map((section, sectionIndex) => (
|
||||||
|
<div key={section.label}>
|
||||||
|
<div className={styles.dropdownHeader} style={{ color: theme.palette.themePrimary }}>
|
||||||
|
{section.label}
|
||||||
|
</div>
|
||||||
|
<div className={styles.dropdownStack}>
|
||||||
|
{section.options.map((option, index) => (
|
||||||
|
<Button
|
||||||
|
key={option}
|
||||||
|
ref={(el) => (itemRefs.current[indexMap.get(`${sectionIndex}-${index}`)] = el)}
|
||||||
|
appearance="transparent"
|
||||||
|
shape="square"
|
||||||
|
className={styles.dropdownOption}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(option);
|
||||||
|
setShowDropdown(false);
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}}
|
||||||
|
onBlur={() =>
|
||||||
|
!bottomLink &&
|
||||||
|
sectionIndex === dropdownOptions.length - 1 &&
|
||||||
|
index === section.options.length - 1 &&
|
||||||
|
setShowDropdown(false)
|
||||||
|
}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) =>
|
||||||
|
handleDownDropdownItemKeyDown(e, indexMap.get(`${sectionIndex}-${index}`))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{bottomLink && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<div className={styles.bottomSection}>
|
||||||
|
<Link
|
||||||
|
ref={(el) => (itemRefs.current[flatIndex] = el)}
|
||||||
|
href={bottomLink.url}
|
||||||
|
target="_blank"
|
||||||
|
onBlur={() => setShowDropdown(false)}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent<HTMLAnchorElement>) => handleDownDropdownItemKeyDown(e, flatIndex)}
|
||||||
|
>
|
||||||
|
{bottomLink.text}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PopoverSurface>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -386,22 +386,6 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
it("should render the page", () => {
|
it("should render the page", () => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking on Edit filter should render the Apply Filter button", () => {
|
|
||||||
wrapper
|
|
||||||
.findWhere((node) => node.text() === "Edit Filter")
|
|
||||||
.at(0)
|
|
||||||
.simulate("click");
|
|
||||||
expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clicking on Edit filter should render input for filter", () => {
|
|
||||||
wrapper
|
|
||||||
.findWhere((node) => node.text() === "Edit Filter")
|
|
||||||
.at(0)
|
|
||||||
.simulate("click");
|
|
||||||
expect(wrapper.find("Input.filterInput").exists()).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Command bar buttons", () => {
|
describe("Command bar buttons", () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
|
||||||
Link,
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarBody,
|
MessageBarBody,
|
||||||
@@ -10,8 +9,7 @@ import {
|
|||||||
makeStyles,
|
makeStyles,
|
||||||
shorthands,
|
shorthands,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { Dismiss16Filled } from "@fluentui/react-icons";
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { createDocument } from "Common/dataAccess/createDocument";
|
import { createDocument } from "Common/dataAccess/createDocument";
|
||||||
@@ -26,6 +24,7 @@ import { Platform, configContext } from "ConfigContext";
|
|||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
@@ -74,6 +73,7 @@ const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we
|
|||||||
const NO_SQL_THROTTLING_DOC_URL =
|
const NO_SQL_THROTTLING_DOC_URL =
|
||||||
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
||||||
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
|
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
|
||||||
|
const DATA_EXPLORER_DOC_URL = "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer";
|
||||||
|
|
||||||
const loadMoreHeight = LayoutConstants.rowHeight;
|
const loadMoreHeight = LayoutConstants.rowHeight;
|
||||||
export const useDocumentsTabStyles = makeStyles({
|
export const useDocumentsTabStyles = makeStyles({
|
||||||
@@ -90,12 +90,6 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
},
|
},
|
||||||
filterInput: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
appliedFilter: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
tableContainer: {
|
tableContainer: {
|
||||||
marginRight: tokens.spacingHorizontalXXXL,
|
marginRight: tokens.spacingHorizontalXXXL,
|
||||||
},
|
},
|
||||||
@@ -556,8 +550,6 @@ export interface IDocumentsTabComponentProps {
|
|||||||
isTabActive: boolean;
|
isTabActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
|
|
||||||
|
|
||||||
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
||||||
['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat(
|
['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat(
|
||||||
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
|
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
|
||||||
@@ -583,14 +575,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onIsExecutingChange,
|
onIsExecutingChange,
|
||||||
isTabActive,
|
isTabActive,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [isFilterCreated, setIsFilterCreated] = useState<boolean>(true);
|
|
||||||
const [isFilterExpanded, setIsFilterExpanded] = useState<boolean>(false);
|
|
||||||
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
|
||||||
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
|
||||||
const [filterContent, setFilterContent] = useState<string>("");
|
const [filterContent, setFilterContent] = useState<string>("");
|
||||||
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
const filterInput = useRef<HTMLInputElement>(null);
|
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
@@ -657,12 +644,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFilterFocused) {
|
|
||||||
filterInput.current?.focus();
|
|
||||||
}
|
|
||||||
}, [isFilterFocused]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively delete all documents by retrying throttled requests (429).
|
* Recursively delete all documents by retrying throttled requests (429).
|
||||||
* This only works for NoSQL, because the bulk response includes status for each delete document request.
|
* This only works for NoSQL, because the bulk response includes status for each delete document request.
|
||||||
@@ -756,11 +737,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}, timeout);
|
}, timeout);
|
||||||
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
||||||
|
|
||||||
const applyFilterButton = {
|
|
||||||
enabled: true,
|
|
||||||
visible: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKey: DataModels.PartitionKey = useMemo(
|
const partitionKey: DataModels.PartitionKey = useMemo(
|
||||||
() => _partitionKey || (_collection && _collection.partitionKey),
|
() => _partitionKey || (_collection && _collection.partitionKey),
|
||||||
[_collection, _partitionKey],
|
[_collection, _partitionKey],
|
||||||
@@ -831,10 +807,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
// This is executed in onActivate() in the original code.
|
// This is executed in onActivate() in the original code.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setKeyboardActions({
|
setKeyboardActions({
|
||||||
[KeyboardAction.SEARCH]: () => {
|
|
||||||
onShowFilterClick();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||||
setFilterContent("");
|
setFilterContent("");
|
||||||
refreshDocumentsGrid(true);
|
refreshDocumentsGrid(true);
|
||||||
@@ -1317,12 +1289,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onShowFilterClick = () => {
|
|
||||||
setIsFilterCreated(true);
|
|
||||||
setIsFilterExpanded(true);
|
|
||||||
setIsFilterFocused(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryTimeoutEnabled = useCallback(
|
const queryTimeoutEnabled = useCallback(
|
||||||
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
||||||
[isPreferredApiMongoDB],
|
[isPreferredApiMongoDB],
|
||||||
@@ -1364,19 +1330,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedColumnIds,
|
selectedColumnIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onHideFilterClick = (): void => {
|
|
||||||
setIsFilterExpanded(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCloseButtonKeyDown: KeyboardEventHandler<HTMLSpanElement> = (event) => {
|
|
||||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
|
||||||
onHideFilterClick();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
|
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
|
||||||
setDocumentIds(newDocumentsIds);
|
setDocumentIds(newDocumentsIds);
|
||||||
|
|
||||||
@@ -1518,14 +1471,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === Constants.NormalizedEventKey.Enter) {
|
||||||
onApplyFilterClick();
|
onApplyFilterClick();
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
onHideFilterClick();
|
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
// Suppress the default behavior of the key
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -2023,10 +1971,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
applyFilterButtonPressed,
|
applyFilterButtonPressed,
|
||||||
});
|
});
|
||||||
|
|
||||||
// collapse filter
|
|
||||||
setAppliedFilter(filterContent);
|
|
||||||
setIsFilterExpanded(false);
|
|
||||||
|
|
||||||
// If apply filter is pressed, reset current selected document
|
// If apply filter is pressed, reset current selected document
|
||||||
if (applyFilterButtonPressed) {
|
if (applyFilterButtonPressed) {
|
||||||
setClickedRowIndex(RESET_INDEX);
|
setClickedRowIndex(RESET_INDEX);
|
||||||
@@ -2103,168 +2047,129 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
|
|
||||||
|
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
||||||
|
const options: InputDatalistDropdownOptionSection[] = [];
|
||||||
|
const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== "");
|
||||||
|
if (nonBlankLastFilters.length > 0) {
|
||||||
|
options.push({
|
||||||
|
label: "Saved filters",
|
||||||
|
options: nonBlankLastFilters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
options.push({
|
||||||
|
label: "Default filters",
|
||||||
|
options: isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CosmosFluentProvider className={styles.container}>
|
<CosmosFluentProvider className={styles.container}>
|
||||||
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||||
{isFilterCreated && (
|
<div className={styles.filterRow}>
|
||||||
<>
|
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||||
{!isFilterExpanded && !isPreferredApiMongoDB && (
|
<InputDataList
|
||||||
<div className={styles.filterRow}>
|
dropdownOptions={getFilterChoices()}
|
||||||
<span>SELECT * FROM c</span>
|
placeholder={
|
||||||
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
isPreferredApiMongoDB
|
||||||
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
||||||
Edit Filter
|
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||||
</Button>
|
}
|
||||||
</div>
|
title="Type a query predicate or choose one from the list."
|
||||||
)}
|
value={filterContent}
|
||||||
{!isFilterExpanded && isPreferredApiMongoDB && (
|
onChange={(value) => setFilterContent(value)}
|
||||||
<div className={styles.filterRow}>
|
onKeyDown={onFilterKeyDown}
|
||||||
{appliedFilter.length > 0 && <span>Filter :</span>}
|
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||||
{!(appliedFilter.length > 0) && <span className="noFilterApplied">No filter applied</span>}
|
/>
|
||||||
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
<Button
|
||||||
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
appearance="primary"
|
||||||
Edit Filter
|
size="small"
|
||||||
</Button>
|
onClick={() => {
|
||||||
</div>
|
if (isExecuting) {
|
||||||
)}
|
if (!isPreferredApiMongoDB) {
|
||||||
{isFilterExpanded && (
|
queryAbortController.abort();
|
||||||
<div className={styles.filterRow}>
|
}
|
||||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
} else {
|
||||||
<Input
|
onApplyFilterClick();
|
||||||
ref={filterInput}
|
}
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
list={`filtersList-${getUniqueId(_collection)}`}
|
|
||||||
className={`filterInput ${styles.filterInput}`}
|
|
||||||
title="Type a query predicate or choose one from the list."
|
|
||||||
placeholder={
|
|
||||||
isPreferredApiMongoDB
|
|
||||||
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
|
||||||
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
|
||||||
}
|
|
||||||
value={filterContent}
|
|
||||||
autoFocus={true}
|
|
||||||
onKeyDown={onFilterKeyDown}
|
|
||||||
onChange={(e) => setFilterContent(e.target.value)}
|
|
||||||
onBlur={() => setIsFilterFocused(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
|
||||||
{addStringsNoDuplicate(
|
|
||||||
lastFilterContents,
|
|
||||||
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
|
||||||
).map((filter) => (
|
|
||||||
<option key={filter} value={filter} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
appearance="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={onApplyFilterClick}
|
|
||||||
disabled={!applyFilterButton.enabled}
|
|
||||||
aria-label="Apply filter"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</Button>
|
|
||||||
{!isPreferredApiMongoDB && isExecuting && (
|
|
||||||
<Button
|
|
||||||
appearance="primary"
|
|
||||||
size="small"
|
|
||||||
aria-label="Cancel Query"
|
|
||||||
onClick={() => queryAbortController.abort()}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Cancel Query
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
aria-label="close filter"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={onHideFilterClick}
|
|
||||||
onKeyDown={onCloseButtonKeyDown}
|
|
||||||
appearance="transparent"
|
|
||||||
size="small"
|
|
||||||
icon={<Dismiss16Filled />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* <Split> doesn't like to be a flex child */}
|
|
||||||
<div style={{ overflow: "hidden", height: "100%" }}>
|
|
||||||
<Allotment
|
|
||||||
onDragEnd={(sizes: number[]) => {
|
|
||||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
|
||||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
|
||||||
setTabStateData(tabStateData);
|
|
||||||
}}
|
}}
|
||||||
|
disabled={isExecuting && isPreferredApiMongoDB}
|
||||||
|
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
|
||||||
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
</Button>
|
||||||
<div className={styles.tableContainer}>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
height: "100%",
|
|
||||||
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
|
||||||
} /* Fix to make table not resize beyond parent's width */
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DocumentsTableComponent
|
|
||||||
onRefreshTable={() => refreshDocumentsGrid(false)}
|
|
||||||
items={tableItems}
|
|
||||||
onSelectedRowsChange={onSelectedRowsChange}
|
|
||||||
selectedRows={selectedRows}
|
|
||||||
size={tableContainerSizePx}
|
|
||||||
selectedColumnIds={selectedColumnIds}
|
|
||||||
columnDefinitions={columnDefinitions}
|
|
||||||
isRowSelectionDisabled={
|
|
||||||
isBulkDeleteDisabled ||
|
|
||||||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
|
||||||
}
|
|
||||||
onColumnSelectionChange={onColumnSelectionChange}
|
|
||||||
defaultColumnSelection={getInitialColumnSelection()}
|
|
||||||
collection={_collection}
|
|
||||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{tableItems.length > 0 && (
|
|
||||||
<a
|
|
||||||
className={styles.loadMore}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
|
||||||
onKeyDown={onLoadMoreKeyInput}
|
|
||||||
>
|
|
||||||
Load more
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Allotment.Pane>
|
|
||||||
<Allotment.Pane minSize={30}>
|
|
||||||
<div style={{ height: "100%", width: "100%" }}>
|
|
||||||
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
|
||||||
<EditorReact
|
|
||||||
language={"json"}
|
|
||||||
content={selectedDocumentContent}
|
|
||||||
isReadOnly={false}
|
|
||||||
ariaLabel={"Document editor"}
|
|
||||||
lineNumbers={"on"}
|
|
||||||
theme={"_theme"}
|
|
||||||
onContentChanged={_onEditorContentChange}
|
|
||||||
enableWordWrapContextMenuItem={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedRows.size > 1 && (
|
|
||||||
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Allotment.Pane>
|
|
||||||
</Allotment>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Allotment
|
||||||
|
onDragEnd={(sizes: number[]) => {
|
||||||
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
|
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
|
setTabStateData(tabStateData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
||||||
|
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
height: "100%",
|
||||||
|
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
||||||
|
} /* Fix to make table not resize beyond parent's width */
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DocumentsTableComponent
|
||||||
|
onRefreshTable={() => refreshDocumentsGrid(false)}
|
||||||
|
items={tableItems}
|
||||||
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
size={tableContainerSizePx}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
columnDefinitions={columnDefinitions}
|
||||||
|
isRowSelectionDisabled={
|
||||||
|
isBulkDeleteDisabled ||
|
||||||
|
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||||
|
}
|
||||||
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
|
defaultColumnSelection={getInitialColumnSelection()}
|
||||||
|
collection={_collection}
|
||||||
|
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tableItems.length > 0 && (
|
||||||
|
<a
|
||||||
|
className={styles.loadMore}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
||||||
|
onKeyDown={onLoadMoreKeyInput}
|
||||||
|
>
|
||||||
|
Load more
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Allotment.Pane>
|
||||||
|
<Allotment.Pane minSize={30}>
|
||||||
|
<div style={{ height: "100%", width: "100%" }}>
|
||||||
|
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
||||||
|
<EditorReact
|
||||||
|
language={"json"}
|
||||||
|
content={selectedDocumentContent}
|
||||||
|
isReadOnly={false}
|
||||||
|
ariaLabel={"Document editor"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
theme={"_theme"}
|
||||||
|
onContentChanged={_onEditorContentChange}
|
||||||
|
enableWordWrapContextMenuItem={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedRows.size > 1 && (
|
||||||
|
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Allotment.Pane>
|
||||||
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
{bulkDeleteOperation && (
|
{bulkDeleteOperation && (
|
||||||
<ProgressModalDialog
|
<ProgressModalDialog
|
||||||
|
|||||||
@@ -17,106 +17,124 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
SELECT * FROM c
|
SELECT * FROM c
|
||||||
</span>
|
</span>
|
||||||
<span
|
<InputDataList
|
||||||
className="___r7kt3y0_0000000 fqerorx"
|
bottomLink={
|
||||||
|
{
|
||||||
|
"text": "Learn more",
|
||||||
|
"url": "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dropdownOptions={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "Default filters",
|
||||||
|
"options": [
|
||||||
|
"WHERE c.id = "foo"",
|
||||||
|
"ORDER BY c._ts DESC",
|
||||||
|
"WHERE c.id = "foo" ORDER BY c._ts DESC",
|
||||||
|
"ORDER BY c._ts ASC",
|
||||||
|
"WHERE c.foo = "foo"",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||||
|
title="Type a query predicate or choose one from the list."
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
|
aria-label="Apply filter"
|
||||||
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
size="small"
|
size="small"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
Edit Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Allotment
|
||||||
style={
|
onDragEnd={[Function]}
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"overflow": "hidden",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Allotment
|
<Allotment.Pane
|
||||||
onDragEnd={[Function]}
|
minSize={55}
|
||||||
|
preferredSize="35%"
|
||||||
>
|
>
|
||||||
<Allotment.Pane
|
<div
|
||||||
minSize={55}
|
style={
|
||||||
preferredSize="35%"
|
{
|
||||||
|
"height": "100%",
|
||||||
|
"overflow": "hidden",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
className="___9o87uj0_0000000 ffefeo0"
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"overflow": "hidden",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___9o87uj0_0000000 ffefeo0"
|
style={
|
||||||
|
{
|
||||||
|
"height": "100%",
|
||||||
|
"width": "calc(100% + -11px)",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<DocumentsTableComponent
|
||||||
style={
|
collection={
|
||||||
{
|
{
|
||||||
"height": "100%",
|
"databaseId": "databaseId",
|
||||||
"width": "calc(100% + -11px)",
|
"id": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
columnDefinitions={
|
||||||
<DocumentsTableComponent
|
[
|
||||||
collection={
|
|
||||||
{
|
{
|
||||||
"databaseId": "databaseId",
|
"id": "id",
|
||||||
"id": [Function],
|
"isPartitionKey": false,
|
||||||
}
|
"label": "id",
|
||||||
}
|
},
|
||||||
columnDefinitions={
|
]
|
||||||
[
|
}
|
||||||
{
|
defaultColumnSelection={
|
||||||
"id": "id",
|
[
|
||||||
"isPartitionKey": false,
|
"id",
|
||||||
"label": "id",
|
]
|
||||||
},
|
}
|
||||||
]
|
isColumnSelectionDisabled={false}
|
||||||
}
|
isRowSelectionDisabled={true}
|
||||||
defaultColumnSelection={
|
items={[]}
|
||||||
[
|
onColumnSelectionChange={[Function]}
|
||||||
"id",
|
onRefreshTable={[Function]}
|
||||||
]
|
onSelectedRowsChange={[Function]}
|
||||||
}
|
selectedColumnIds={
|
||||||
isColumnSelectionDisabled={false}
|
[
|
||||||
isRowSelectionDisabled={true}
|
"id",
|
||||||
items={[]}
|
]
|
||||||
onColumnSelectionChange={[Function]}
|
}
|
||||||
onRefreshTable={[Function]}
|
selectedRows={Set {}}
|
||||||
onSelectedRowsChange={[Function]}
|
/>
|
||||||
selectedColumnIds={
|
|
||||||
[
|
|
||||||
"id",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
selectedRows={Set {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Allotment.Pane>
|
</div>
|
||||||
<Allotment.Pane
|
</Allotment.Pane>
|
||||||
minSize={30}
|
<Allotment.Pane
|
||||||
>
|
minSize={30}
|
||||||
<div
|
>
|
||||||
style={
|
<div
|
||||||
{
|
style={
|
||||||
"height": "100%",
|
{
|
||||||
"width": "100%",
|
"height": "100%",
|
||||||
}
|
"width": "100%",
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
</Allotment.Pane>
|
/>
|
||||||
</Allotment>
|
</Allotment.Pane>
|
||||||
</div>
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export interface UserContext {
|
|||||||
readonly endpoint?: string;
|
readonly endpoint?: string;
|
||||||
readonly aadToken?: string;
|
readonly aadToken?: string;
|
||||||
readonly accessToken?: string;
|
readonly accessToken?: string;
|
||||||
|
readonly armToken?: string;
|
||||||
readonly authorizationToken?: string;
|
readonly authorizationToken?: string;
|
||||||
readonly resourceToken?: string;
|
readonly resourceToken?: string;
|
||||||
readonly subscriptionType?: SubscriptionType;
|
readonly subscriptionType?: SubscriptionType;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useBoolean } from "@fluentui/react-hooks";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
|
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||||
|
import { updateUserContext } from "UserContext";
|
||||||
|
|
||||||
const msalInstance = await getMsalInstance();
|
const msalInstance = await getMsalInstance();
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ export function useAADAuth(): ReturnType {
|
|||||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
||||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||||
});
|
});
|
||||||
|
updateUserContext({ armToken: armToken });
|
||||||
setArmToken(armToken);
|
setArmToken(armToken);
|
||||||
setAuthFailure(null);
|
setAuthFailure(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { HttpHeaders } from "Common/Constants";
|
import { HttpHeaders } from "Common/Constants";
|
||||||
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
|
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
|
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
@@ -33,12 +35,9 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
|
|||||||
return accounts.sort((a, b) => a.name.localeCompare(b.name));
|
return accounts.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDatabaseAccountsFromGraph(
|
export async function fetchDatabaseAccountsFromGraph(subscriptionId: string): Promise<DatabaseAccount[]> {
|
||||||
subscriptionId: string,
|
|
||||||
accessToken: string,
|
|
||||||
): Promise<DatabaseAccount[]> {
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
const bearer = `Bearer ${accessToken}`;
|
const bearer = `Bearer ${userContext.armToken}`;
|
||||||
|
|
||||||
headers.append("Authorization", bearer);
|
headers.append("Authorization", bearer);
|
||||||
headers.append(HttpHeaders.contentType, "application/json");
|
headers.append(HttpHeaders.contentType, "application/json");
|
||||||
@@ -46,8 +45,9 @@ export async function fetchDatabaseAccountsFromGraph(
|
|||||||
const apiVersion = "2021-03-01";
|
const apiVersion = "2021-03-01";
|
||||||
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
|
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
|
||||||
|
|
||||||
const databaseAccounts: DatabaseAccount[] = [];
|
let databaseAccounts: DatabaseAccount[] = [];
|
||||||
let skipToken: string;
|
let skipToken: string;
|
||||||
|
console.log("Old ARM Token - fetchDatabaseAccountsFromGraph function", userContext.armToken);
|
||||||
do {
|
do {
|
||||||
const body = {
|
const body = {
|
||||||
query: databaseAccountsQuery,
|
query: databaseAccountsQuery,
|
||||||
@@ -85,10 +85,81 @@ export async function fetchDatabaseAccountsFromGraph(
|
|||||||
return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name));
|
return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined {
|
export function useDatabaseAccounts(subscriptionId: string): DatabaseAccount[] | undefined {
|
||||||
const { data } = useSWR(
|
const { data } = useSWR(
|
||||||
() => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined),
|
() => (subscriptionId ? ["databaseAccounts", subscriptionId] : undefined),
|
||||||
(_, subscriptionId, armToken) => fetchDatabaseAccountsFromGraph(subscriptionId, armToken),
|
(_, subscriptionId) => runCommand(fetchDatabaseAccountsFromGraph, subscriptionId),
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define the types for your responses
|
||||||
|
interface DatabaseAccount {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
// Add other relevant fields as per your use case
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryRequestOptions {
|
||||||
|
$top?: number;
|
||||||
|
$skipToken?: string;
|
||||||
|
$allowPartialScopes?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the configuration context and headers if not already defined
|
||||||
|
const configContext = {
|
||||||
|
ARM_ENDPOINT: "https://management.azure.com/",
|
||||||
|
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface QueryResponse {
|
||||||
|
data?: any[];
|
||||||
|
$skipToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCommand<T>(fn: (...args: any[]) => Promise<T>, ...args: any[]): Promise<T> {
|
||||||
|
try {
|
||||||
|
// Attempt to execute the function passed as an argument
|
||||||
|
const result = await fn(...args);
|
||||||
|
console.log("Successfully executed function:", fn.name, result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle any error that is thrown during the execution of the function
|
||||||
|
if (error) {
|
||||||
|
console.log("Creating new token");
|
||||||
|
const msalInstance = await getMsalInstance();
|
||||||
|
|
||||||
|
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||||
|
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||||
|
|
||||||
|
msalInstance.setActiveAccount(cachedAccount);
|
||||||
|
|
||||||
|
// TODO: Add condition to check if the ARM token needs to be renewed, then we need to run the code below for creating the ARM token
|
||||||
|
|
||||||
|
console.log("Creating new ARM token");
|
||||||
|
const newAccessToken = await acquireTokenWithMsal(msalInstance, {
|
||||||
|
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
|
||||||
|
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||||
|
});
|
||||||
|
updateUserContext({ armToken: newAccessToken });
|
||||||
|
|
||||||
|
// TODO: add condition to check if AAD token needs to be renewed (i.e) Token provider has failed with expired AAD token and create a new AAD Token using the below code
|
||||||
|
|
||||||
|
// const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/$/, "/.default");
|
||||||
|
// console.log('Creating new AAD token');
|
||||||
|
// let aadToken = await acquireTokenWithMsal(msalInstance, {
|
||||||
|
// forceRefresh: true,
|
||||||
|
// authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
|
||||||
|
// scopes: [hrefEndpoint],
|
||||||
|
// });
|
||||||
|
// updateUserContext({aadToken: aadToken});
|
||||||
|
|
||||||
|
//console.log('Latest AAD Token', fn.name, userContext.aadToken);
|
||||||
|
const result = await fn(...args);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
console.error("An error occurred:", error.message);
|
||||||
|
throw new error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { Subscription } from "../Contracts/DataModels";
|
import { Subscription } from "../Contracts/DataModels";
|
||||||
|
import { runCommand } from "hooks/useDatabaseAccounts";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
interface SubscriptionListResult {
|
interface SubscriptionListResult {
|
||||||
@@ -35,9 +37,9 @@ export async function fetchSubscriptions(accessToken: string): Promise<Subscript
|
|||||||
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<Subscription[]> {
|
export async function fetchSubscriptionsFromGraph(): Promise<Subscription[]> {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
const bearer = `Bearer ${accessToken}`;
|
const bearer = `Bearer ${userContext.armToken}`;
|
||||||
|
|
||||||
headers.append("Authorization", bearer);
|
headers.append("Authorization", bearer);
|
||||||
headers.append(HttpHeaders.contentType, "application/json");
|
headers.append(HttpHeaders.contentType, "application/json");
|
||||||
@@ -48,6 +50,7 @@ export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<
|
|||||||
|
|
||||||
const subscriptions: Subscription[] = [];
|
const subscriptions: Subscription[] = [];
|
||||||
let skipToken: string;
|
let skipToken: string;
|
||||||
|
console.log("Old ARM Token fetchSubscriptionsFromGraph fn", userContext.armToken);
|
||||||
do {
|
do {
|
||||||
const body = {
|
const body = {
|
||||||
query: subscriptionsQuery,
|
query: subscriptionsQuery,
|
||||||
@@ -86,9 +89,14 @@ export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSubscriptions(armToken: string): Subscription[] | undefined {
|
export function useSubscriptions(armToken: string): Subscription[] | undefined {
|
||||||
const { data } = useSWR(
|
const { data, error } = useSWR(
|
||||||
() => (armToken ? ["subscriptions", armToken] : undefined),
|
() => (armToken ? ["subscriptions", armToken] : undefined),
|
||||||
(_, armToken) => fetchSubscriptionsFromGraph(armToken),
|
(_) => runCommand(fetchSubscriptionsFromGraph),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching subscriptions:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user