Azure Monitor: Add support for multiple template variables in resource picker (#46215)
This commit is contained in:
+9
-1
@@ -27,9 +27,11 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
const ctx: any = {};
|
const ctx: any = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
templateSrv.init([singleVariable]);
|
||||||
ctx.instanceSettings = {
|
ctx.instanceSettings = {
|
||||||
jsonData: { subscriptionId: 'xxx' },
|
jsonData: { subscriptionId: 'xxx' },
|
||||||
url: 'http://azureloganalyticsapi',
|
url: 'http://azureloganalyticsapi',
|
||||||
|
templateSrv: templateSrv,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
||||||
@@ -76,10 +78,11 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getSchema', () => {
|
describe('When performing getSchema', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
ctx.mockGetResource = jest.fn().mockImplementation((path: string) => {
|
||||||
expect(path).toContain('metadata');
|
expect(path).toContain('metadata');
|
||||||
return Promise.resolve(FakeSchemaData.getlogAnalyticsFakeMetadata());
|
return Promise.resolve(FakeSchemaData.getlogAnalyticsFakeMetadata());
|
||||||
});
|
});
|
||||||
|
ctx.ds.azureLogAnalyticsDatasource.getResource = ctx.mockGetResource;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a schema to use with monaco-kusto', async () => {
|
it('should return a schema to use with monaco-kusto', async () => {
|
||||||
@@ -112,6 +115,11 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should interpolate variables when making a request for a schema with a uri that contains template variables', async () => {
|
||||||
|
await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace/$var1');
|
||||||
|
expect(ctx.mockGetResource).lastCalledWith('loganalytics/v1myWorkspace/var1-foo/metadata');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When performing annotationQuery', () => {
|
describe('When performing annotationQuery', () => {
|
||||||
|
|||||||
+4
-2
@@ -109,8 +109,10 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getKustoSchema(resourceUri: string) {
|
async getKustoSchema(resourceUri: string) {
|
||||||
const metadata = await this.getMetadata(resourceUri);
|
const templateSrv = getTemplateSrv();
|
||||||
return transformMetadataToKustoSchema(metadata, resourceUri);
|
const interpolatedUri = templateSrv.replace(resourceUri, {}, interpolateVariable);
|
||||||
|
const metadata = await this.getMetadata(interpolatedUri);
|
||||||
|
return transformMetadataToKustoSchema(metadata, interpolatedUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): AzureMonitorQuery {
|
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): AzureMonitorQuery {
|
||||||
|
|||||||
-3
@@ -47,8 +47,6 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
|
|||||||
[closePicker, onQueryChange, query]
|
[closePicker, onQueryChange, query]
|
||||||
);
|
);
|
||||||
|
|
||||||
const templateVariables = datasource.getVariables();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -63,7 +61,6 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
|
|||||||
<ResourcePicker
|
<ResourcePicker
|
||||||
resourcePickerData={datasource.resourcePickerData}
|
resourcePickerData={datasource.resourcePickerData}
|
||||||
resourceURI={resource}
|
resourceURI={resource}
|
||||||
templateVariables={templateVariables}
|
|
||||||
onApply={handleApply}
|
onApply={handleApply}
|
||||||
onCancel={closePicker}
|
onCancel={closePicker}
|
||||||
selectableEntryTypes={[
|
selectableEntryTypes={[
|
||||||
|
|||||||
+12
-7
@@ -1,4 +1,5 @@
|
|||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ResourcePicker from '.';
|
import ResourcePicker from '.';
|
||||||
@@ -87,21 +88,25 @@ describe('AzureMonitor ResourcePicker', () => {
|
|||||||
expect(onApply).toBeCalledTimes(1);
|
expect(onApply).toBeCalledTimes(1);
|
||||||
expect(onApply).toBeCalledWith('/subscriptions/def-123');
|
expect(onApply).toBeCalledWith('/subscriptions/def-123');
|
||||||
});
|
});
|
||||||
it('should call onApply with a template variable when a user selects it', async () => {
|
|
||||||
|
it('should call onApply with a new subscription uri when a user types it', async () => {
|
||||||
const onApply = jest.fn();
|
const onApply = jest.fn();
|
||||||
render(<ResourcePicker {...defaultProps} templateVariables={['$workspace']} onApply={onApply} />);
|
render(<ResourcePicker {...defaultProps} onApply={onApply} />);
|
||||||
|
const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
|
||||||
|
expect(subscriptionCheckbox).toBeInTheDocument();
|
||||||
|
expect(subscriptionCheckbox).not.toBeChecked();
|
||||||
|
|
||||||
const expandButton = await screen.findByLabelText('Expand Template variables');
|
const advancedSection = screen.getByText('Advanced');
|
||||||
expandButton.click();
|
advancedSection.click();
|
||||||
|
|
||||||
const workSpaceCheckbox = await screen.findByLabelText('$workspace');
|
const advancedInput = await screen.findByLabelText('Resource URI');
|
||||||
workSpaceCheckbox.click();
|
userEvent.type(advancedInput, '/subscriptions/def-123');
|
||||||
|
|
||||||
const applyButton = screen.getByRole('button', { name: 'Apply' });
|
const applyButton = screen.getByRole('button', { name: 'Apply' });
|
||||||
applyButton.click();
|
applyButton.click();
|
||||||
|
|
||||||
expect(onApply).toBeCalledTimes(1);
|
expect(onApply).toBeCalledTimes(1);
|
||||||
expect(onApply).toBeCalledWith('$workspace');
|
expect(onApply).toBeCalledWith('/subscriptions/def-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when rendering resource picker without any selectable entry types', () => {
|
describe('when rendering resource picker without any selectable entry types', () => {
|
||||||
|
|||||||
+48
-38
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Alert, Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
import { Alert, Button, Icon, Input, LoadingPlaceholder, Tooltip, useStyles2, Collapse, Label } from '@grafana/ui';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import ResourcePickerData from '../../resourcePicker/resourcePickerData';
|
import ResourcePickerData from '../../resourcePicker/resourcePickerData';
|
||||||
@@ -10,11 +10,9 @@ import NestedResourceTable from './NestedResourceTable';
|
|||||||
import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types';
|
import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types';
|
||||||
import { addResources, findRow, parseResourceURI } from './utils';
|
import { addResources, findRow, parseResourceURI } from './utils';
|
||||||
|
|
||||||
const TEMPLATE_VARIABLE_GROUP_ID = '$$grafana-templateVariables$$';
|
|
||||||
interface ResourcePickerProps {
|
interface ResourcePickerProps {
|
||||||
resourcePickerData: ResourcePickerData;
|
resourcePickerData: ResourcePickerData;
|
||||||
resourceURI: string | undefined;
|
resourceURI: string | undefined;
|
||||||
templateVariables: string[];
|
|
||||||
selectableEntryTypes: ResourceRowType[];
|
selectableEntryTypes: ResourceRowType[];
|
||||||
|
|
||||||
onApply: (resourceURI: string | undefined) => void;
|
onApply: (resourceURI: string | undefined) => void;
|
||||||
@@ -24,7 +22,6 @@ interface ResourcePickerProps {
|
|||||||
const ResourcePicker = ({
|
const ResourcePicker = ({
|
||||||
resourcePickerData,
|
resourcePickerData,
|
||||||
resourceURI,
|
resourceURI,
|
||||||
templateVariables,
|
|
||||||
onApply,
|
onApply,
|
||||||
onCancel,
|
onCancel,
|
||||||
selectableEntryTypes,
|
selectableEntryTypes,
|
||||||
@@ -36,7 +33,7 @@ const ResourcePicker = ({
|
|||||||
const [azureRows, setAzureRows] = useState<ResourceRowGroup>([]);
|
const [azureRows, setAzureRows] = useState<ResourceRowGroup>([]);
|
||||||
const [internalSelectedURI, setInternalSelectedURI] = useState<string | undefined>(resourceURI);
|
const [internalSelectedURI, setInternalSelectedURI] = useState<string | undefined>(resourceURI);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
|
const [isAdvancedOpen, setIsAdvancedOpen] = useState(resourceURI?.includes('$'));
|
||||||
// Sync the resourceURI prop to internal state
|
// Sync the resourceURI prop to internal state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInternalSelectedURI(resourceURI);
|
setInternalSelectedURI(resourceURI);
|
||||||
@@ -85,14 +82,10 @@ const ResourcePicker = ({
|
|||||||
}
|
}
|
||||||
}, [resourcePickerData, internalSelectedURI, azureRows, loadingStatus]);
|
}, [resourcePickerData, internalSelectedURI, azureRows, loadingStatus]);
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
|
||||||
const templateVariableRow = transformVariablesToRow(templateVariables);
|
|
||||||
return templateVariables.length ? [...azureRows, templateVariableRow] : azureRows;
|
|
||||||
}, [azureRows, templateVariables]);
|
|
||||||
|
|
||||||
// Map the selected item into an array of rows
|
// Map the selected item into an array of rows
|
||||||
const selectedResourceRows = useMemo(() => {
|
const selectedResourceRows = useMemo(() => {
|
||||||
const found = internalSelectedURI && findRow(rows, internalSelectedURI);
|
const found = internalSelectedURI && findRow(azureRows, internalSelectedURI);
|
||||||
|
|
||||||
return found
|
return found
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -101,7 +94,7 @@ const ResourcePicker = ({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
}, [internalSelectedURI, rows]);
|
}, [internalSelectedURI, azureRows]);
|
||||||
|
|
||||||
// Request resources for a expanded resource group
|
// Request resources for a expanded resource group
|
||||||
const requestNestedRows = useCallback(
|
const requestNestedRows = useCallback(
|
||||||
@@ -109,12 +102,8 @@ const ResourcePicker = ({
|
|||||||
// clear error message (also when loading cached resources)
|
// clear error message (also when loading cached resources)
|
||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
|
|
||||||
// If we already have children, we don't need to re-fetch them. Also abort if we're expanding the special
|
// If we already have children, we don't need to re-fetch them.
|
||||||
// template variable group, though that shouldn't happen in practice
|
if (resourceGroupOrSubscription.children?.length) {
|
||||||
if (
|
|
||||||
resourceGroupOrSubscription.children?.length ||
|
|
||||||
resourceGroupOrSubscription.uri === TEMPLATE_VARIABLE_GROUP_ID
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +141,7 @@ const ResourcePicker = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<NestedResourceTable
|
<NestedResourceTable
|
||||||
rows={rows}
|
rows={azureRows}
|
||||||
requestNestedRows={requestNestedRows}
|
requestNestedRows={requestNestedRows}
|
||||||
onRowSelectedChange={handleSelectionChanged}
|
onRowSelectedChange={handleSelectionChanged}
|
||||||
selectedRows={selectedResourceRows}
|
selectedRows={selectedResourceRows}
|
||||||
@@ -162,7 +151,6 @@ const ResourcePicker = ({
|
|||||||
<div className={styles.selectionFooter}>
|
<div className={styles.selectionFooter}>
|
||||||
{selectedResourceRows.length > 0 && (
|
{selectedResourceRows.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Space v={2} />
|
|
||||||
<h5>Selection</h5>
|
<h5>Selection</h5>
|
||||||
<NestedResourceTable
|
<NestedResourceTable
|
||||||
rows={selectedResourceRows}
|
rows={selectedResourceRows}
|
||||||
@@ -172,15 +160,54 @@ const ResourcePicker = ({
|
|||||||
noHeader={true}
|
noHeader={true}
|
||||||
selectableEntryTypes={selectableEntryTypes}
|
selectableEntryTypes={selectableEntryTypes}
|
||||||
/>
|
/>
|
||||||
|
<Space v={2} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Collapse
|
||||||
|
collapsible
|
||||||
|
label="Advanced"
|
||||||
|
isOpen={isAdvancedOpen}
|
||||||
|
onToggle={() => setIsAdvancedOpen(!isAdvancedOpen)}
|
||||||
|
>
|
||||||
|
<Label htmlFor={`input-${internalSelectedURI}`}>
|
||||||
|
<h6>
|
||||||
|
Resource URI{' '}
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
Manually edit the{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.microsoft.com/en-us/azure/azure-monitor/logs/log-standard-columns#_resourceid"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
resource uri.{' '}
|
||||||
|
</a>
|
||||||
|
Supports the use of multiple template variables (ex: /subscriptions/$subId/resourceGroups/$rg)
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
interactive={true}
|
||||||
|
>
|
||||||
|
<Icon name="info-circle" />
|
||||||
|
</Tooltip>
|
||||||
|
</h6>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`input-${internalSelectedURI}`}
|
||||||
|
value={internalSelectedURI}
|
||||||
|
onChange={(event) => setInternalSelectedURI(event.currentTarget.value)}
|
||||||
|
placeholder="ex: /subscriptions/$subId"
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
<Space v={2} />
|
<Space v={2} />
|
||||||
|
|
||||||
<Button disabled={!!errorMessage} onClick={handleApply}>
|
<Button disabled={!!errorMessage} onClick={handleApply}>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Space layout="inline" h={1} />
|
<Space layout="inline" h={1} />
|
||||||
|
|
||||||
<Button onClick={onCancel} variant="secondary">
|
<Button onClick={onCancel} variant="secondary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@@ -215,20 +242,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
color: theme.colors.text.secondary,
|
color: theme.colors.text.secondary,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
function transformVariablesToRow(templateVariables: string[]): ResourceRow {
|
|
||||||
return {
|
|
||||||
id: TEMPLATE_VARIABLE_GROUP_ID,
|
|
||||||
uri: TEMPLATE_VARIABLE_GROUP_ID,
|
|
||||||
name: 'Template variables',
|
|
||||||
type: ResourceRowType.VariableGroup,
|
|
||||||
typeLabel: 'Variables',
|
|
||||||
children: templateVariables.map((v) => ({
|
|
||||||
id: v,
|
|
||||||
uri: v,
|
|
||||||
name: v,
|
|
||||||
type: ResourceRowType.Variable,
|
|
||||||
typeLabel: 'Variable',
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-2
@@ -48,9 +48,12 @@ export function addResources(rows: ResourceRowGroup, targetParentId: string, new
|
|||||||
return produce(rows, (draftState) => {
|
return produce(rows, (draftState) => {
|
||||||
const draftRow = findRow(draftState, targetParentId);
|
const draftRow = findRow(draftState, targetParentId);
|
||||||
|
|
||||||
|
// we can't find the selected resource in our list of resources,
|
||||||
|
// probably means user has either mistyped in the input field
|
||||||
|
// or is using template variables.
|
||||||
|
// either way no need to throw, just show that none of the resources are checked
|
||||||
if (!draftRow) {
|
if (!draftRow) {
|
||||||
// This case shouldn't happen often because we're usually coming here from a resource we already have
|
return;
|
||||||
throw new Error('Unable to find resource');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draftRow.children = newResources;
|
draftRow.children = newResources;
|
||||||
|
|||||||
+1
-12
@@ -92,7 +92,7 @@ describe('AzureMonitor resourcePickerData', () => {
|
|||||||
await resourcePickerData.getSubscriptions();
|
await resourcePickerData.getSubscriptions();
|
||||||
throw Error('expected getSubscriptions to fail but it succeeded');
|
throw Error('expected getSubscriptions to fail but it succeeded');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err.message).toEqual('unable to fetch subscriptions');
|
expect(err.message).toEqual('No subscriptions were found');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -163,17 +163,6 @@ describe('AzureMonitor resourcePickerData', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if it does not receive data', async () => {
|
|
||||||
const mockResponse = { data: [] };
|
|
||||||
const { resourcePickerData } = createResourcePickerData([mockResponse]);
|
|
||||||
try {
|
|
||||||
await resourcePickerData.getResourceGroupsBySubscriptionId('123');
|
|
||||||
throw Error('expected getSubscriptions to fail but it succeeded');
|
|
||||||
} catch (err) {
|
|
||||||
expect(err.message).toEqual('unable to fetch resource groups');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error if it recieves data with a malformed uri', async () => {
|
it('throws an error if it recieves data with a malformed uri', async () => {
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
data: [
|
data: [
|
||||||
|
|||||||
+11
-6
@@ -57,7 +57,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
|
|||||||
}
|
}
|
||||||
const resourceResponse = await this.makeResourceGraphRequest<RawAzureSubscriptionItem[]>(query, 1, options);
|
const resourceResponse = await this.makeResourceGraphRequest<RawAzureSubscriptionItem[]>(query, 1, options);
|
||||||
if (!resourceResponse.data.length) {
|
if (!resourceResponse.data.length) {
|
||||||
throw new Error('unable to fetch subscriptions');
|
throw new Error('No subscriptions were found');
|
||||||
}
|
}
|
||||||
resources = resources.concat(resourceResponse.data);
|
resources = resources.concat(resourceResponse.data);
|
||||||
$skipToken = resourceResponse.$skipToken;
|
$skipToken = resourceResponse.$skipToken;
|
||||||
@@ -100,9 +100,6 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query, 1, options);
|
const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query, 1, options);
|
||||||
if (!resourceResponse.data.length) {
|
|
||||||
throw new Error('unable to fetch resource groups');
|
|
||||||
}
|
|
||||||
resourceGroups = resourceGroups.concat(resourceResponse.data);
|
resourceGroups = resourceGroups.concat(resourceResponse.data);
|
||||||
$skipToken = resourceResponse.$skipToken;
|
$skipToken = resourceResponse.$skipToken;
|
||||||
allFetched = !$skipToken;
|
allFetched = !$skipToken;
|
||||||
@@ -150,7 +147,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
|
|||||||
|
|
||||||
// used to make the select resource button that launches the resource picker show a nicer file path to users
|
// used to make the select resource button that launches the resource picker show a nicer file path to users
|
||||||
async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
|
async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
|
||||||
const { subscriptionID, resourceGroup } = parseResourceURI(resourceURI) ?? {};
|
const { subscriptionID, resourceGroup, resource } = parseResourceURI(resourceURI) ?? {};
|
||||||
|
|
||||||
if (!subscriptionID) {
|
if (!subscriptionID) {
|
||||||
throw new Error('Invalid resource URI passed');
|
throw new Error('Invalid resource URI passed');
|
||||||
@@ -189,7 +186,15 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
|
|||||||
throw new Error('unable to fetch resource details');
|
throw new Error('unable to fetch resource details');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response[0];
|
const { subscriptionName, resourceGroupName, resourceName } = response[0];
|
||||||
|
// if the name is undefined it could be because the id is undefined or because we are using a template variable.
|
||||||
|
// Either way we can use it as a fallback. We don't really want to interpolate these variables because we want
|
||||||
|
// to show the user when they are using template variables `$sub/$rg/$resource`
|
||||||
|
return {
|
||||||
|
subscriptionName: subscriptionName || subscriptionID,
|
||||||
|
resourceGroupName: resourceGroupName || resourceGroup,
|
||||||
|
resourceName: resourceName || resource,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourceURIFromWorkspace(workspace: string) {
|
async getResourceURIFromWorkspace(workspace: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user