Skip to content

Commit

Permalink
NAS-132487: Add shell to containers (#11036)
Browse files Browse the repository at this point in the history
  • Loading branch information
undsoft authored Nov 15, 2024
1 parent b423d43 commit 8766977
Show file tree
Hide file tree
Showing 102 changed files with 464 additions and 32 deletions.
23 changes: 16 additions & 7 deletions src/app/interfaces/terminal.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ export interface TerminalConfiguration {
connectionData: TerminalConnectionData;
}

export interface TerminalConnectionData {
vmId?: number;
podInfo?: {
chartReleaseName: string;
containerId: string;
export type TerminalConnectionData =
// VMs
| {
vm_id: number;
}
// Virtualization instances
| {
virt_instance_id: string;
}
// Apps
| {
app_name: string;
container_id: string;
command: string;
};
}
}
// No params
| Record<string, never>;
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export class TerminalComponent implements OnInit, OnDestroy {
}

initializeWebShell(): void {
this.shellService.connect(this.conf.connectionData, this.token);
this.shellService.connect(this.token, this.conf.connectionData);

this.shellService.shellConnected$.pipe(untilDestroyed(this)).subscribe((event: ShellConnectedEvent) => {
this.shellConnected = event.connected;
Expand All @@ -212,7 +212,7 @@ export class TerminalComponent implements OnInit, OnDestroy {
}

reconnect(): void {
this.shellService.connect(this.conf.connectionData, this.token);
this.shellService.connect(this.token, this.conf.connectionData);
}

onFontSizeChanged(newSize: number): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ export class ContainerShellComponent implements TerminalConfiguration {

get connectionData(): TerminalConnectionData {
return {
podInfo: {
chartReleaseName: this.appName,
containerId: this.containerId,
command: this.command,
},
app_name: this.appName,
container_id: this.containerId,
command: this.command,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ <h3 class="title">
<ix-instance-devices></ix-instance-devices>
<ix-instance-disks></ix-instance-disks>
<ix-instance-proxies></ix-instance-proxies>

<ix-instance-tools [instance]="instance()"></ix-instance-tools>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
import {
InstanceProxiesComponent,
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-proxies/instance-proxies.component';
import {
InstanceToolsComponent,
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-tools/instance-tools.component';
import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/virtualization-instances.store';

@Component({
Expand All @@ -30,6 +33,7 @@ import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/vi
InstanceGeneralInfoComponent,
InstanceProxiesComponent,
InstanceDisksComponent,
InstanceToolsComponent,
MobileBackButtonComponent,
],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<mat-card class="card">
<mat-card-header>
<h3 mat-card-title>
{{ 'Tools' | translate }}
</h3>
</mat-card-header>

<mat-card-content>
<div [matTooltip]="isInstanceStopped() ? ('Instance is not running' | translate) : ''">
<a
mat-flat-button
color="default"
ixTest="open-shell"
class="tool"
[disabled]="isInstanceStopped()"
[routerLink]="['/virtualization', 'view', instance().id, 'shell']"
>
<span>{{ 'Shell' | translate }}</span>

<ix-icon name="mdi-console"></ix-icon>
</a>
</div>
</mat-card-content>
</mat-card>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.tool {
display: flex;
margin: 0 -16px;

::ng-deep .mdc-button__label {
flex-grow: 1;
justify-content: space-between;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { VirtualizationStatus } from 'app/enums/virtualization.enum';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import {
InstanceToolsComponent,
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-tools/instance-tools.component';

describe('InstanceToolsComponent', () => {
let spectator: Spectator<InstanceToolsComponent>;
let loader: HarnessLoader;
const createComponent = createComponentFactory({
component: InstanceToolsComponent,
});

beforeEach(() => {
spectator = createComponent({
props: {
instance: {
id: 'my-instance',
status: VirtualizationStatus.Running,
} as VirtualizationInstance,
},
});

loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

describe('shell', () => {
it('shows a link to shell', async () => {
const shellLink = await loader.getHarness(MatButtonHarness.with({ text: 'Shell' }));

expect(shellLink).toBeTruthy();
expect(await (await shellLink.host()).getAttribute('href')).toBe('/virtualization/view/my-instance/shell');
});

it('show shell link as disabled when instance is not running', async () => {
spectator.setInput('instance', {
id: 'my-instance',
status: VirtualizationStatus.Stopped,
} as VirtualizationInstance);

const shellLink = await loader.getHarness(MatButtonHarness.with({ text: 'Shell' }));
expect(await shellLink.isDisabled()).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
ChangeDetectionStrategy, Component, computed, input,
} from '@angular/core';
import { MatAnchor, MatButton } from '@angular/material/button';
import {
MatCard, MatCardContent, MatCardHeader, MatCardTitle,
} from '@angular/material/card';
import { MatTooltip } from '@angular/material/tooltip';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { VirtualizationStatus } from 'app/enums/virtualization.enum';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component';
import { TestDirective } from 'app/modules/test-id/test.directive';

@Component({
selector: 'ix-instance-tools',
templateUrl: './instance-tools.component.html',
styleUrls: ['./instance-tools.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MatCardTitle,
MatCardHeader,
MatCard,
MatCardContent,
TranslateModule,
MatButton,
MatAnchor,
TestDirective,
IxIconComponent,
MatTooltip,
RouterLink,
],
})
export class InstanceToolsComponent {
readonly instance = input.required<VirtualizationInstance>();

protected readonly isInstanceStopped = computed(() => this.instance().status !== VirtualizationStatus.Running);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Observable, Subscriber } from 'rxjs';
import { TerminalConfiguration, TerminalConnectionData } from 'app/interfaces/terminal.interface';
import { TerminalComponent } from 'app/modules/terminal/components/terminal/terminal.component';

@UntilDestroy()
@Component({
selector: 'ix-instance-shell',
template: '<ix-terminal [conf]="this"></ix-terminal>',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TerminalComponent],
})
export class InstanceShellComponent implements TerminalConfiguration {
protected instanceId = signal('');

get connectionData(): TerminalConnectionData {
return {
virt_instance_id: this.instanceId(),
};
}

constructor(
private aroute: ActivatedRoute,
) {}

preInit(): Observable<void> {
return new Observable<void>((subscriber: Subscriber<void>) => {
this.aroute.params.pipe(untilDestroyed(this)).subscribe((params) => {
this.instanceId.set(params['id'] as string);
subscriber.next();
});
});
}
}
7 changes: 7 additions & 0 deletions src/app/pages/virtualization/virtualization.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Routes } from '@angular/router';
import { marker as T } from '@biesbjerg/ngx-translate-extract-marker';
import { AllInstancesComponent } from 'app/pages/virtualization/components/all-instances/all-instances.component';
import { InstanceShellComponent } from 'app/pages/virtualization/components/instance-shell/instance-shell.component';
import { InstanceWizardComponent } from 'app/pages/virtualization/components/instance-wizard/instance-wizard.component';
import { VirtualizationConfigStore } from 'app/pages/virtualization/stores/virtualization-config.store';
import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/virtualization-instances.store';
Expand All @@ -23,12 +24,18 @@ export const virtualizationRoutes: Routes = [{
},
{
path: 'view/:id',
data: { breadcrumb: T('Containers') },
children: [
{
path: '',
pathMatch: 'full',
component: AllInstancesComponent,
},
{
path: 'shell',
component: InstanceShellComponent,
data: { title: T('Instance Shell') },
},
],
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class VmSerialShellComponent implements TerminalConfiguration {

get connectionData(): TerminalConnectionData {
return {
vmId: Number(this.pk),
vm_id: Number(this.pk),
};
}

Expand Down
21 changes: 4 additions & 17 deletions src/app/services/shell.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ export class ShellService {
@Inject(WEBSOCKET) private webSocket: typeof rxjsWebSocket,
) {}

connect(connectionData: TerminalConnectionData, token: string): void {
connect(token: string, connectionData: TerminalConnectionData): void {
this.disconnectIfSessionActive();

this.ws$ = this.webSocket({
url: this.connectionUrl,
openObserver: {
next: () => this.onOpen(connectionData, token),
next: () => this.onOpen(token, connectionData),
},
closeObserver: {
next: this.onClose.bind(this),
Expand All @@ -60,21 +60,8 @@ export class ShellService {
});
}

private onOpen(connectionData: TerminalConnectionData, token: string): void {
if (connectionData.vmId) {
this.ws$.next(JSON.stringify({ token, options: { vm_id: connectionData.vmId } }));
} else if (connectionData.podInfo) {
this.ws$.next(JSON.stringify({
token,
options: {
app_name: connectionData.podInfo.chartReleaseName,
container_id: connectionData.podInfo.containerId,
command: connectionData.podInfo.command,
},
}));
} else {
this.ws$.next(JSON.stringify({ token }));
}
private onOpen(token: string, connectionData: TerminalConnectionData): void {
this.ws$.next(JSON.stringify({ token, options: connectionData }));
}

private onClose(): void {
Expand Down
3 changes: 3 additions & 0 deletions src/assets/i18n/af.json
Original file line number Diff line number Diff line change
Expand Up @@ -2298,7 +2298,9 @@
"Instance Configuration": "",
"Instance Port": "",
"Instance Protocol": "",
"Instance Shell": "",
"Instance created": "",
"Instance is not running": "",
"Instance updated": "",
"Instances you created will automatically appear here.": "",
"Integrate Snapshots with VMware": "",
Expand Down Expand Up @@ -4564,6 +4566,7 @@
"Token expired": "",
"Tolerance Window": "",
"Toolbar": "",
"Tools": "",
"Top": "",
"Top bar": "",
"Top level of the LDAP directory tree to be used when searching for resources. Example: <i>dc=test,dc=org</i>.": "",
Expand Down
3 changes: 3 additions & 0 deletions src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -2298,7 +2298,9 @@
"Instance Configuration": "",
"Instance Port": "",
"Instance Protocol": "",
"Instance Shell": "",
"Instance created": "",
"Instance is not running": "",
"Instance updated": "",
"Instances you created will automatically appear here.": "",
"Integrate Snapshots with VMware": "",
Expand Down Expand Up @@ -4564,6 +4566,7 @@
"Token expired": "",
"Tolerance Window": "",
"Toolbar": "",
"Tools": "",
"Top": "",
"Top bar": "",
"Top level of the LDAP directory tree to be used when searching for resources. Example: <i>dc=test,dc=org</i>.": "",
Expand Down
3 changes: 3 additions & 0 deletions src/assets/i18n/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -2298,7 +2298,9 @@
"Instance Configuration": "",
"Instance Port": "",
"Instance Protocol": "",
"Instance Shell": "",
"Instance created": "",
"Instance is not running": "",
"Instance updated": "",
"Instances you created will automatically appear here.": "",
"Integrate Snapshots with VMware": "",
Expand Down Expand Up @@ -4564,6 +4566,7 @@
"Token expired": "",
"Tolerance Window": "",
"Toolbar": "",
"Tools": "",
"Top": "",
"Top bar": "",
"Top level of the LDAP directory tree to be used when searching for resources. Example: <i>dc=test,dc=org</i>.": "",
Expand Down
3 changes: 3 additions & 0 deletions src/assets/i18n/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -2298,7 +2298,9 @@
"Instance Configuration": "",
"Instance Port": "",
"Instance Protocol": "",
"Instance Shell": "",
"Instance created": "",
"Instance is not running": "",
"Instance updated": "",
"Instances you created will automatically appear here.": "",
"Integrate Snapshots with VMware": "",
Expand Down Expand Up @@ -4564,6 +4566,7 @@
"Token expired": "",
"Tolerance Window": "",
"Toolbar": "",
"Tools": "",
"Top": "",
"Top bar": "",
"Top level of the LDAP directory tree to be used when searching for resources. Example: <i>dc=test,dc=org</i>.": "",
Expand Down
Loading

0 comments on commit 8766977

Please sign in to comment.