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