1import React, { useCallback, useEffect, useMemo, useState } from 'react';
2import NestedResourceTable from './NestedResourceTable';
3import { ResourceRow, ResourceRowGroup } from './types';
4import { css } from '@emotion/css';
5import { GrafanaTheme2 } from '@grafana/data';
6import { Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
7import ResourcePickerData from '../../resourcePicker/resourcePickerData';
8import { Space } from '../Space';
9import { addResources, findRow, parseResourceURI } from './utils';
10
11interface ResourcePickerProps {
12  resourcePickerData: ResourcePickerData;
13  resourceURI: string | undefined;
14  templateVariables: string[];
15
16  onApply: (resourceURI: string | undefined) => void;
17  onCancel: () => void;
18}
19
20const ResourcePicker = ({
21  resourcePickerData,
22  resourceURI,
23  templateVariables,
24  onApply,
25  onCancel,
26}: ResourcePickerProps) => {
27  const styles = useStyles2(getStyles);
28
29  const [azureRows, setAzureRows] = useState<ResourceRowGroup>([]);
30  const [internalSelected, setInternalSelected] = useState<string | undefined>(resourceURI);
31  const [isLoading, setIsLoading] = useState(false);
32
33  // Sync the resourceURI prop to internal state
34  useEffect(() => {
35    setInternalSelected(resourceURI);
36  }, [resourceURI]);
37
38  const rows = useMemo(() => {
39    const templateVariableRow = resourcePickerData.transformVariablesToRow(templateVariables);
40    return templateVariables.length ? [...azureRows, templateVariableRow] : azureRows;
41  }, [resourcePickerData, azureRows, templateVariables]);
42
43  // Map the selected item into an array of rows
44  const selectedResourceRows = useMemo(() => {
45    const found = internalSelected && findRow(rows, internalSelected);
46    return found
47      ? [
48          {
49            ...found,
50            children: undefined,
51          },
52        ]
53      : [];
54  }, [internalSelected, rows]);
55
56  // Request resources for a expanded resource group
57  const requestNestedRows = useCallback(
58    async (resourceGroup: ResourceRow) => {
59      // If we already have children, we don't need to re-fetch them. Also abort if we're expanding the special
60      // template variable group, though that shouldn't happen in practice
61      if (resourceGroup.children?.length || resourceGroup.id === ResourcePickerData.templateVariableGroupID) {
62        return;
63      }
64
65      // fetch and set nested resources for the resourcegroup into the bigger state object
66      const resources = await resourcePickerData.getResourcesForResourceGroup(resourceGroup);
67      const newRows = addResources(azureRows, resourceGroup.id, resources);
68      setAzureRows(newRows);
69    },
70    [resourcePickerData, azureRows]
71  );
72
73  // Select
74  const handleSelectionChanged = useCallback((row: ResourceRow, isSelected: boolean) => {
75    isSelected ? setInternalSelected(row.id) : setInternalSelected(undefined);
76  }, []);
77
78  // Request initial data on first mount
79  useEffect(() => {
80    setIsLoading(true);
81    resourcePickerData.getResourcePickerData().then((initalRows) => {
82      setIsLoading(false);
83      setAzureRows(initalRows);
84    });
85  }, [resourcePickerData]);
86
87  // Request sibling resources for a selected resource - in practice should only be on first mount
88  useEffect(() => {
89    if (!internalSelected || !rows.length) {
90      return;
91    }
92
93    // If we can find this resource in the rows, then we don't need to load anything
94    const foundResourceRow = findRow(rows, internalSelected);
95    if (foundResourceRow) {
96      return;
97    }
98
99    const parsedURI = parseResourceURI(internalSelected);
100    const resourceGroupURI = `/subscriptions/${parsedURI?.subscriptionID}/resourceGroups/${parsedURI?.resourceGroup}`;
101    const resourceGroupRow = findRow(rows, resourceGroupURI);
102
103    if (!resourceGroupRow) {
104      // We haven't loaded the data from Azure yet
105      return;
106    }
107
108    requestNestedRows(resourceGroupRow);
109  }, [requestNestedRows, internalSelected, rows]);
110
111  const handleApply = useCallback(() => {
112    onApply(internalSelected);
113  }, [internalSelected, onApply]);
114
115  return (
116    <div>
117      {isLoading ? (
118        <div className={styles.loadingWrapper}>
119          <LoadingPlaceholder text={'Loading resources...'} />
120        </div>
121      ) : (
122        <>
123          <NestedResourceTable
124            rows={rows}
125            requestNestedRows={requestNestedRows}
126            onRowSelectedChange={handleSelectionChanged}
127            selectedRows={selectedResourceRows}
128          />
129
130          <div className={styles.selectionFooter}>
131            {selectedResourceRows.length > 0 && (
132              <>
133                <Space v={2} />
134                <h5>Selection</h5>
135                <NestedResourceTable
136                  rows={selectedResourceRows}
137                  requestNestedRows={requestNestedRows}
138                  onRowSelectedChange={handleSelectionChanged}
139                  selectedRows={selectedResourceRows}
140                  noHeader={true}
141                />
142              </>
143            )}
144
145            <Space v={2} />
146
147            <Button onClick={handleApply}>Apply</Button>
148            <Space layout="inline" h={1} />
149            <Button onClick={onCancel} variant="secondary">
150              Cancel
151            </Button>
152          </div>
153        </>
154      )}
155    </div>
156  );
157};
158
159export default ResourcePicker;
160
161const getStyles = (theme: GrafanaTheme2) => ({
162  selectionFooter: css({
163    position: 'sticky',
164    bottom: 0,
165    background: theme.colors.background.primary,
166    paddingTop: theme.spacing(2),
167  }),
168  loadingWrapper: css({
169    textAlign: 'center',
170    paddingTop: theme.spacing(2),
171    paddingBottom: theme.spacing(2),
172    color: theme.colors.text.secondary,
173  }),
174});
175