Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(armview): Auto-select resource under cursor #60

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "C:\\temp\\stuff"],
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceRoot}\\test"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: watch"
}
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"eslint.format.enable": true,
"[typescript]": {
Expand Down
90 changes: 71 additions & 19 deletions assets/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,35 @@ function init(prefix) {
// Handle selection events
cy.on('select', evt => {
// Only work with nodes, user can't select edges/arrows
if (evt.target.isNode()) {

// Force selection of single nodes only
if (cy.$('node:selected').length > 1) {
cy.$('node:selected')[0].unselect()
if (evt.target.isNode() || evt.target.isEdge()) {
if (evt.target.isEdge()) {
document.getElementById('infobox-heading').innerHTML = '<img id="infoimg" src=\'\'/>&nbsp; Dependency Details'
document.getElementById('infoimg').setAttribute('src', iconPrefix + '/default.svg')
}
else {
document.getElementById('infobox-heading').innerHTML = '<img id="infoimg" src=\'\'/>&nbsp; Details'
document.getElementById('infoimg').setAttribute('src', iconPrefix + '/' + evt.target.data('img'))
}

// The rest of this is just pulling info from the node's data and showing it in a HTML div & table
document.getElementById('infoimg').setAttribute('src', iconPrefix + '/' + evt.target.data('img'))

document.getElementById('infotable').innerHTML = ''
_addInfo('Name', evt.target.data('name'))
_addInfo('Type', evt.target.data('type'))
_addInfo('Location', evt.target.data('location'))
if (evt.target.data('kind'))
_addInfo('Kind', evt.target.data('kind'))

// Display any extra fields
if (evt.target.data('extra')) {
Object.keys(evt.target.data('extra')).forEach(extra => {
_addInfo(extra, evt.target.data('extra')[extra])
})

// The rest of this is just pulling info from the node's data and showing it in a HTML div & table
if (evt.target.isEdge()) {
_addInfo('Source', cy.$(`node[id = "${evt.target.data('source')}"]`).data('name'))
_addInfo('Target', cy.$(`node[id = "${evt.target.data('target')}"]`).data('name'))
} else {
_addInfo('Name', evt.target.data('name'))
_addInfo('Type', evt.target.data('type'))
_addInfo('Location', evt.target.data('location'))
if (evt.target.data('kind'))
_addInfo('Kind', evt.target.data('kind'))

// Display any extra fields
if (evt.target.data('extra')) {
Object.keys(evt.target.data('extra')).forEach(extra => {
_addInfo(extra, evt.target.data('extra')[extra])
})
}
}

// Now display the info box
Expand Down Expand Up @@ -176,6 +183,12 @@ function reLayout(mode, animate) {
'opacity': 0.6
})

// Edges are arrows between resources
cy.style().selector('edge:selected').style({
'line-color': invertColor(borderColor),
'target-arrow-color': invertColor(borderColor),
})

// Bounding box for groups
cy.style().selector(':parent').style({
'background-image': 'none',
Expand All @@ -196,6 +209,13 @@ function reLayout(mode, animate) {
'text-outline-width': '4'
})

// Bounding box for selected nodes
cy.style().selector(':parent:selected').style({
'border-color': invertColor(borderColor),
'background-color': invertColor(borderColor),
'background-opacity': 0.1,
})

// Set up snap to grid
cy.snapToGrid({ gridSpacing: 200, lineWidth: 3, drawGrid: false })
if (settingSnap)
Expand Down Expand Up @@ -289,6 +309,21 @@ function toggleSnap() {
}
}

function invertColor(rgb) {
// Extract the numeric parts of the RGB values
const rgbValues = rgb.match(/\d+/g)

if (!rgbValues) {
return null // or throw an error if the format is incorrect
}

// Invert each color component
const inverted = rgbValues.map(component => 255 - parseInt(component, 10))

// Format the inverted colors back into the 'rgb(r, g, b)' format
return `rgb(${inverted.join(', ')})`
}

//
// **** VS Code extension WebView specific functions below here ****
//
Expand Down Expand Up @@ -345,6 +380,23 @@ window.addEventListener('message', event => {
document.getElementById('statusResCount').innerHTML = message.payload
}
}

// highlight an element
if (message.command == 'selectRes') {
hideInfo()
cy.$('*').unselect()

if (message.payload) {
if (message.payload.group == 'nodes') {
cy.$(`node[id = "${message.payload.data.id}"]`).select()
}
else {
cy.$(`node[id = "${message.payload.data.source}"]`).select()
cy.$(`node[id = "${message.payload.data.target}"]`).select()
cy.$(`edge[id = "${message.payload.data.id}"]`).select()
}
}
}
})

//
Expand Down
37 changes: 29 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@
"jsonc-parser": "^3.0.0",
"node-cache": "^5.1.2",
"uuid": "^8.3.2",
"vscode-extension-telemetry": "^0.1.6"
"vscode-extension-telemetry": "^0.1.6",
"acorn": "^8.11.3",
"acorn-walk": "^8.3.2"
},
"devDependencies": {
"@types/uuid": "^8.3.4",
Expand Down
54 changes: 53 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as path from 'path'
import ARMParser from './lib/arm-parser'
import TelemetryReporter from 'vscode-extension-telemetry'
import * as NodeCache from 'node-cache'
import { CytoscapeNode } from './lib/arm-parser-types'

// Set up telemetry logging
// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down Expand Up @@ -137,6 +138,15 @@ export function activate(context: vscode.ExtensionContext): void {
}
})

// listen for cursor position changes
vscode.window.onDidChangeTextEditorSelection(async () => {
try {
refreshSelection()
} catch (err) {
// Nadda
}
})

// Handle messages from the webview
panel.webview.onDidReceiveMessage(
message => {
Expand Down Expand Up @@ -231,6 +241,46 @@ async function pickFilters(): Promise<any> {
//
// Refresh contents of the view
//

async function refreshSelection(parser: ARMParser|null = null): Promise<any> {
if (!panel) {
return
}

if (!editor) {
vscode.window.showErrorMessage('No editor active, open a ARM template JSON file in the editor')
return
}

if(parser == null) {
// Skip non-JSON
if (!(editor.document.languageId === 'json' || editor.document.languageId === 'arm-template')) {
return
}

// Parse the source template JSON
const templateJSON = editor.document.getText()

// Create a new ARM parser, giving icon prefix based on theme, and name it "main"
// Additionally passing reporter and editor enables telemetry and linked template discovery in VS Code workspace
parser = new ARMParser(`${extensionPath}/assets/img/azure/${themeName}`, 'main', reporter, editor, cache)

try {
await parser.parse(templateJSON, paramFileContent)
} catch (err: any) {
return
}
}

let activeElement: CytoscapeNode | null = null
try {
const offset = editor.document.offsetAt(editor.selection.active)
activeElement = parser.getActiveElement(offset)
} finally {
panel.webview.postMessage({ command: 'selectRes', payload: activeElement })
}
}

async function refreshView(): Promise<any> {
// Reset timers for typing updates
refreshedTime = Date.now()
Expand Down Expand Up @@ -267,6 +317,8 @@ async function refreshView(): Promise<any> {
})
panel.webview.postMessage({ command: 'newData', payload: result })
panel.webview.postMessage({ command: 'resCount', payload: result.length.toString() })

refreshSelection(parser)
} catch (err: any) {
// Disable logging and telemetry for now
// console.log('### ArmView: ERROR STACK: ' + err.stack)
Expand Down Expand Up @@ -346,7 +398,7 @@ function getWebviewContent(): string {
</div>

<div id="infobox">
<div class="panel-heading" onclick="hideInfo()"><img id="infoimg" src=''/> &nbsp; Resource Details</div>
<div id="infobox-heading" class="panel-heading" onclick="hideInfo()"><img id="infoimg" src=''/> &nbsp; Resource Details</div>
<div class="panel-body">
<table id="infotable"></table>
</div>
Expand Down
Loading