Skip to content

Commit

Permalink
feat: new dependency graph
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszjenek committed Nov 21, 2024
1 parent 489826f commit b8a78a4
Show file tree
Hide file tree
Showing 27 changed files with 24,181 additions and 1,110 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ jobs:
with:
node-version: 18.x

- name: Authenticate with private NPM package
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc

- name: Build graph webview
working-directory: ./webview/resource-dependency-graph
run: npm ci

- run: npm ci

- run: npm run lint
Expand Down
2,412 changes: 1,361 additions & 1,051 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions src/adapters/humctl/HumctlAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,20 @@ export class HumctlAdapter implements IHumctlAdapter {
result.stderr.includes('HTTP-403')
) {
throw new UnauthorizedError();
} else if (result.stderr.includes('environment is required')) {
} else if (
result.stderr.includes('environment is required') ||
result.stderr.includes('missing env')
) {
throw new NotEnoughContextError(HumanitecContext.ENV);
} else if (result.stderr.includes('application is required')) {
} else if (
result.stderr.includes('application is required') ||
result.stderr.includes('missing app')
) {
throw new NotEnoughContextError(HumanitecContext.APP);
} else if (result.stderr.includes('organization is required')) {
} else if (
result.stderr.includes('organization is required') ||
result.stderr.includes('missing org')
) {
throw new NotEnoughContextError(HumanitecContext.ORG);
} else if (result.stderr.includes('no deployments in environment')) {
throw new NoDeploymentsInEnvironmentError(
Expand Down
141 changes: 117 additions & 24 deletions src/controllers/DisplayResourceDependencyGraphController.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,146 @@
import * as vscode from 'vscode';
import { IResourceDependencyGraphService } from '../services/ResourcesGraphService';
import { IErrorHandlerService } from '../services/ErrorHandlerService';
import { IActiveResourcesRepository } from '../repos/ActiveResourcesRepository';
import { IDependencyGraphRepository } from '../repos/DependencyGraphRepository';
import { IDeploymentRepository } from '../repos/DeploymentRepository';
import { IResourceTypeRepository } from '../repos/ResourceTypeRepository';
import { IConfigurationRepository } from '../repos/ConfigurationRepository';
import { IResourceDefinitionRepository } from '../repos/ResourceDefinitionRepository';
import { ConfigKey } from '../domain/ConfigKey';
import { IEnvironmentRepository } from '../repos/EnvironmentRepository';
import { NoDeploymentsInEnvironmentError } from '../errors/NoDeploymentsInEnvironmentError';

export class DisplayResourcesGraphController {
private constructor() {}

static register(
context: vscode.ExtensionContext,
resourcesGraphService: IResourceDependencyGraphService,
activeResourcesRepository: IActiveResourcesRepository,
dependencyGraphRepository: IDependencyGraphRepository,
deploymentRepository: IDeploymentRepository,
ResourceDefinitionRepository: IResourceDefinitionRepository,
resourceTypeRepository: IResourceTypeRepository,
environmentRepository: IEnvironmentRepository,
configs: IConfigurationRepository,
errorHandler: IErrorHandlerService
) {
const disposable = vscode.commands.registerCommand(
'humanitec.display_resources_graph',
async () => {
try {
const graph = await resourcesGraphService.generate();
const orgId = await configs.get(ConfigKey.HUMANITEC_ORG);
const appId = await configs.get(ConfigKey.HUMANITEC_APP);
const envId = await configs.get(ConfigKey.HUMANITEC_ENV);

const environment = await environmentRepository.get(
orgId,
appId,
envId
);

if (!environment.lastDeploymentId) {
throw new NoDeploymentsInEnvironmentError(envId);
}

const panel = vscode.window.createWebviewPanel(
'humanitec_resources_graph',
'Resources Graph',
'webview',
'Resource Dependency Graph',
vscode.ViewColumn.One,
{
enableScripts: true,
}
);
panel.webview.html = this.getWebviewContent(graph);
const scriptSrc = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
'webview',
'resource-dependency-graph',
'dist',
'index.js'
)
);

const cssSrc = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
'webview',
'resource-dependency-graph',
'dist',
'index.css'
)
);

panel.webview.onDidReceiveMessage(async msg => {
switch (msg.command) {
case 'resource-dependency-graph-send-data':
try {
const activeResources =
await activeResourcesRepository.getRaw(orgId, appId, envId);

const deployment = await deploymentRepository.getLatestRaw(
orgId,
appId,
envId,
environment.lastDeploymentId!
);
interface DeploymentObject {
dependency_graph_id: string;
}
const dependencyGraph =
await dependencyGraphRepository.getRaw(
orgId,
appId,
envId,
(deployment as DeploymentObject).dependency_graph_id
);
const resourceDefinitions =
await ResourceDefinitionRepository.getAllRaw(orgId);
const resourceTypes =
await resourceTypeRepository.getAvailableRaw(orgId);
panel.webview.postMessage({
type: 'resource-dependency-graph-data',
value: {
activeResources,
deployment,
dependencyGraph,
resourceDefinitions,
resourceTypes,
},
});
} catch (error) {
errorHandler.handle(error);
}
break;
}
});

panel.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="${cssSrc}" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
//declare the vscode variable to be used in the front-end
const vscode = acquireVsCodeApi();
//send a message to confirm everything is working properly
window.onload = function() {
vscode.postMessage({ command: 'resource-dependency-graph-send-data' });
console.log('HTML started up.');
};
</script>
<script src="${scriptSrc}"></script>
</body>
</html>
`;
} catch (error) {
errorHandler.handle(error);
}
}
);
context.subscriptions.push(disposable);
}

private static getWebviewContent(graph: string): string {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/@hpcc-js/[email protected]/dist/index.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/d3-graphviz.js"></script>
<div id="graph" style="text-align: center;"></div>
<script>
d3.select("#graph").graphviz().renderDot(\`${graph}\`);
</script>
</body>
</html>`;
}
}
3 changes: 2 additions & 1 deletion src/domain/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export class Environment {
public readonly id: string,
public readonly name: string,
public readonly organizationId: string,
public readonly applicationId: string
public readonly applicationId: string,
public readonly lastDeploymentId: string | null
) {}
}
17 changes: 15 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { ResourceTypeRepository } from './repos/ResourceTypeRepository';
import { InitializeScoreFileController } from './controllers/InitializeScoreFileController';
import { ScoreInitializationService } from './services/ScoreInitializationService';
import { DisplayResourcesGraphController } from './controllers/DisplayResourceDependencyGraphController';
import { ResourcesGraphService } from './services/ResourcesGraphService';
import { OpenConfiguredTerminalController } from './controllers/OpenConfiguredTerminalController';
import { LoggerService } from './services/LoggerService';
import { OrganizationRepository } from './repos/OrganizationRepository';
Expand All @@ -19,6 +18,10 @@ import { HumctlAdapter } from './adapters/humctl/HumctlAdapter';
import { LoginController } from './controllers/LoginController';
import { LoginService } from './services/LoginService';
import { ErrorHandlerService } from './services/ErrorHandlerService';
import { DeploymentRepository } from './repos/DeploymentRepository';
import { ActiveResourcesRepository } from './repos/ActiveResourcesRepository';
import { DependencyGraphRepository } from './repos/DependencyGraphRepository';
import { ResourceDefinitionRepository } from './repos/ResourceDefinitionRepository';

export const loggerChannel = vscode.window.createOutputChannel('Humanitec');

Expand All @@ -32,6 +35,10 @@ export async function activate(context: vscode.ExtensionContext) {
secretRepository,
context
);
const activeResourcesRepository = new ActiveResourcesRepository(humctl);
const dependencyGraphRepository = new DependencyGraphRepository(humctl);
const deploymentRepository = new DeploymentRepository(humctl);
const resourceDefinitionRepository = new ResourceDefinitionRepository(humctl);
const resourceTypeRepository = new ResourceTypeRepository(humctl);
const organizationRepository = new OrganizationRepository(humctl);
const applicationRepository = new ApplicationRepository(humctl);
Expand Down Expand Up @@ -68,7 +75,13 @@ export async function activate(context: vscode.ExtensionContext) {
);
DisplayResourcesGraphController.register(
context,
new ResourcesGraphService(humctl),
activeResourcesRepository,
dependencyGraphRepository,
deploymentRepository,
resourceDefinitionRepository,
resourceTypeRepository,
environmentRepository,
configurationRepository,
errorHandler
);
OpenConfiguredTerminalController.register(
Expand Down
37 changes: 37 additions & 0 deletions src/repos/ActiveResourcesRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { IHumctlAdapter } from '../adapters/humctl/IHumctlAdapter';
import { HumctlError } from '../errors/HumctlError';

export interface IActiveResourcesRepository {
getRaw(
organizationId: string,
applicationId: string,
environmentId: string
): Promise<unknown>;
}

export class ActiveResourcesRepository implements IActiveResourcesRepository {
constructor(private humctl: IHumctlAdapter) {}
async getRaw(
organizationId: string,
applicationId: string,
environmentId: string
): Promise<unknown> {
const resourcesUrl =
'/orgs/' +
organizationId +
'/apps/' +
applicationId +
'/envs/' +
environmentId +
'/resources';
const result = await this.humctl.execute(['api', 'get', resourcesUrl]);
if (result.stderr !== '') {
throw new HumctlError(
'humctl api get ' + resourcesUrl,
result.stderr,
result.exitcode
);
}
return JSON.parse(result.stdout);
}
}
40 changes: 40 additions & 0 deletions src/repos/DependencyGraphRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IHumctlAdapter } from '../adapters/humctl/IHumctlAdapter';
import { HumctlError } from '../errors/HumctlError';

export interface IDependencyGraphRepository {
getRaw(
organizationId: string,
applicationId: string,
environmentId: string,
dependencyGraphId: string
): Promise<unknown>;
}

export class DependencyGraphRepository implements IDependencyGraphRepository {
constructor(private humctl: IHumctlAdapter) {}
async getRaw(
organizationId: string,
applicationId: string,
environmentId: string,
dependencyGraphId: string
): Promise<unknown> {
const graphUrl =
'/orgs/' +
organizationId +
'/apps/' +
applicationId +
'/envs/' +
environmentId +
'/resources/graphs/' +
dependencyGraphId;
const result = await this.humctl.execute(['api', 'get', graphUrl]);
if (result.stderr !== '') {
throw new HumctlError(
'humctl api get ' + graphUrl,
result.stderr,
result.exitcode
);
}
return JSON.parse(result.stdout);
}
}
41 changes: 41 additions & 0 deletions src/repos/DeploymentRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { IHumctlAdapter } from '../adapters/humctl/IHumctlAdapter';
import { HumctlError } from '../errors/HumctlError';

export interface IDeploymentRepository {
getLatestRaw(
organizationId: string,
applicationId: string,
environmentId: string,
deployId: string
): Promise<unknown>;
}

export class DeploymentRepository implements IDeploymentRepository {
constructor(private humctl: IHumctlAdapter) {}

async getLatestRaw(
organizationId: string,
applicationId: string,
environmentId: string,
deployId: string
): Promise<unknown> {
const deploymentUrl =
'/orgs/' +
organizationId +
'/apps/' +
applicationId +
'/envs/' +
environmentId +
'/deploys/' +
deployId;
const result = await this.humctl.execute(['api', 'get', deploymentUrl]);
if (result.stderr !== '') {
throw new HumctlError(
'humctl api get ' + deploymentUrl,
result.stderr,
result.exitcode
);
}
return JSON.parse(result.stdout);
}
}
Loading

0 comments on commit b8a78a4

Please sign in to comment.