From 72876ccc37d5a3314db9557dcd9addb210420f34 Mon Sep 17 00:00:00 2001 From: Kakeru Ishii Date: Wed, 8 Apr 2026 00:25:19 +0900 Subject: [PATCH 1/2] refactor(web): move module imports to main.ts and add test file - Removed `KHIIconRegistrationModule` and `...environment.pluginModules` from `imports` in `web/src/app/root.component.ts`. - Removed imports of `KHIIconRegistrationModule` and `environment` from `web/src/app/root.component.ts`. - Created `web/src/main.spec.ts` with a test for `appConfig` initialization. - In `web/src/main.ts`, moved the configuration object from `bootstrapApplication` to a new exported `appConfig` constant. - Added `importProvidersFrom(KHIIconRegistrationModule)` and `importProvidersFrom(...environment.pluginModules)` to `appConfig.providers` in `web/src/main.ts`. --- web/src/app/root.component.ts | 8 +------- web/src/main.spec.ts | 37 +++++++++++++++++++++++++++++++++++ web/src/main.ts | 17 ++++++++++++++-- 3 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 web/src/main.spec.ts diff --git a/web/src/app/root.component.ts b/web/src/app/root.component.ts index b71c69a2..9f3e17cb 100644 --- a/web/src/app/root.component.ts +++ b/web/src/app/root.component.ts @@ -15,17 +15,11 @@ */ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; -import { KHIIconRegistrationModule } from './shared/module/icon-registration.module'; -import { environment } from 'src/environments/environment'; @Component({ selector: 'khi-root', templateUrl: './root.component.html', styleUrls: ['./root.component.scss'], - imports: [ - RouterOutlet, - KHIIconRegistrationModule, - ...environment.pluginModules, - ], + imports: [RouterOutlet], }) export class RootComponent {} diff --git a/web/src/main.spec.ts b/web/src/main.spec.ts new file mode 100644 index 00000000..b0488602 --- /dev/null +++ b/web/src/main.spec.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { appConfig } from './main'; +import { ApplicationInitStatus } from '@angular/core'; +import { ConsoleReporter, Reporter } from './app/common/reporter/reporter'; + +describe('main.ts appConfig', () => { + it('should complete application initialization successfully', async () => { + TestBed.configureTestingModule({ + // Provides ConsoleReporter for testing, as it is typically provided by plugins in production. + providers: [ + { provide: Reporter, useClass: ConsoleReporter }, + ...appConfig.providers, + ], + }); + + const status = TestBed.inject(ApplicationInitStatus); + // Waits for all APP_INITIALIZERs to complete. + await status.donePromise; + expect(status.done).toBe(true); + }); +}); diff --git a/web/src/main.ts b/web/src/main.ts index eb83b5f6..04b22beb 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -21,6 +21,8 @@ import { provideAppInitializer, Injector, ErrorHandler, + importProvidersFrom, + ApplicationConfig, } from '@angular/core'; import { ReporterErrorHandler } from './app/common/reporter/reporter-error-handler'; import { bootstrapApplication } from '@angular/platform-browser'; @@ -74,12 +76,17 @@ import { } from './app/extensions/extension-common/extension-store'; import { KHI_FRONTEND_EXTENSION_BUNDLES } from './app/extensions/extension-common/extension'; import { provideAnimations } from '@angular/platform-browser/animations'; +import { KHIIconRegistrationModule } from './app/shared/module/icon-registration.module'; if (environment.production) { enableProdMode(); } -bootstrapApplication(RootComponent, { +/** + * Application configuration for KHI. + * Defines providers used during bootstrap. + */ +export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection(), provideRouter(KHIRoutes), @@ -142,6 +149,8 @@ bootstrapApplication(RootComponent, { }, NotificationManager, DiffPageDataSource, + importProvidersFrom(KHIIconRegistrationModule), + importProvidersFrom(...environment.pluginModules), provideAppInitializer(() => { const extensionStore = inject(EXTENSION_STORE); const notificationManager = inject(NotificationManager); @@ -156,4 +165,8 @@ bootstrapApplication(RootComponent, { notificationManager.initialize(); }), ], -}).catch((err) => console.error(err)); +}; + +bootstrapApplication(RootComponent, appConfig).catch((err) => + console.error(err), +); From 88dd2ec44ae91e2ff861be77c637a82275c688f1 Mon Sep 17 00:00:00 2001 From: Kakeru Ishii Date: Tue, 7 Apr 2026 12:48:57 +0900 Subject: [PATCH 2/2] feat(startup): add startup side menu component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit サイドメニュー(ロゴ、タイトル、イタリック体のキャッチフレーズ、ボタン類)の完成形。 --- .../startup-side-menu.component.html | 62 +++++++ .../startup-side-menu.component.scss | 155 ++++++++++++++++++ .../startup-side-menu.component.spec.ts | 133 +++++++++++++++ .../components/startup-side-menu.component.ts | 51 ++++++ .../components/startup-side-menu.stories.ts | 54 ++++++ .../startup/types/startup-side-menu.types.ts | 27 +++ 6 files changed, 482 insertions(+) create mode 100644 web/src/app/dialogs/startup/components/startup-side-menu.component.html create mode 100644 web/src/app/dialogs/startup/components/startup-side-menu.component.scss create mode 100644 web/src/app/dialogs/startup/components/startup-side-menu.component.spec.ts create mode 100644 web/src/app/dialogs/startup/components/startup-side-menu.component.ts create mode 100644 web/src/app/dialogs/startup/components/startup-side-menu.stories.ts create mode 100644 web/src/app/dialogs/startup/types/startup-side-menu.types.ts diff --git a/web/src/app/dialogs/startup/components/startup-side-menu.component.html b/web/src/app/dialogs/startup/components/startup-side-menu.component.html new file mode 100644 index 00000000..910490e8 --- /dev/null +++ b/web/src/app/dialogs/startup/components/startup-side-menu.component.html @@ -0,0 +1,62 @@ + + +
+
+
+ +
+
+

Kubernetes History Inspector

+ {{ version() }} +
+

A log viewer for Kubernetes troubleshooting

+
+ +
+
+ +

+ Collect and process cluster logs for visual analysis. +

+
+ +
+ +

Import a previously exported inspection file.

+
+
+ + +
diff --git a/web/src/app/dialogs/startup/components/startup-side-menu.component.scss b/web/src/app/dialogs/startup/components/startup-side-menu.component.scss new file mode 100644 index 00000000..e2efc549 --- /dev/null +++ b/web/src/app/dialogs/startup/components/startup-side-menu.component.scss @@ -0,0 +1,155 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Color variables based on the design image +$sidebar-bg-color: #f5f6f9; +$primary-blue: #2f54eb; +$text-dark: #1f2329; +$text-muted: #64748b; +$white: #ffffff; +$border-color: #d9d9d9; +$shadow-color: rgba(0, 0, 0, 0.05); +$icon-bg-alpha: rgba(255, 255, 255, 0.2); + +.side-menu-container { + display: grid; + grid-template: + "brand" auto + "actions" 1fr + "footer" auto + / 100%; + gap: 40px; + + width: 220px; + height: 100%; + padding: 20px; + box-sizing: border-box; + + background-color: $sidebar-bg-color; +} + +.brand { + grid-area: brand; + display: grid; + grid-template: + "logo title" auto + "tagline tagline" auto + / auto 1fr; + gap: 16px; + align-items: center; +} + +.logo-container { + grid-area: logo; + display: grid; + place-items: center; + + width: 48px; + height: 48px; + border-radius: 8px; + + background-color: $white; + box-shadow: 0 2px 8px $shadow-color; +} + +.logo { + width: 32px; + height: 32px; +} + +.title-group { + grid-area: title; + display: grid; + grid-template: + "name" auto + "version" auto + / 100%; + gap: 4px; +} + +.title { + margin: 0; + + font-family: "Inter", sans-serif; + font-size: 18px; + font-weight: 700; + color: $text-dark; +} + +.tagline { + grid-area: tagline; + margin: 0; + + font-size: 12px; + font-style: italic; + color: $text-muted; + line-height: 1.4; +} + +.version { + font-size: 12px; + color: $text-muted; +} + +.actions { + grid-area: actions; + display: grid; + gap: 12px; + align-content: space-around; +} + +.action-item { + display: flex; + flex-direction: column; + gap: 8px; +} + +.description { + margin: 0; + + font-size: 12px; + color: $text-muted; + line-height: 1.5; +} + +.footer-links { + grid-area: footer; + display: grid; + gap: 12px; +} + +.footer-link { + display: grid; + grid-template: "icon text" / auto 1fr; + gap: 8px; + align-items: center; + + font-size: 14px; + color: $text-muted; + text-decoration: none; + + transition: color 0.2s; + + &:hover { + color: $primary-blue; + } + + mat-icon { + width: 18px; + height: 18px; + font-size: 18px; + } +} diff --git a/web/src/app/dialogs/startup/components/startup-side-menu.component.spec.ts b/web/src/app/dialogs/startup/components/startup-side-menu.component.spec.ts new file mode 100644 index 00000000..245200a0 --- /dev/null +++ b/web/src/app/dialogs/startup/components/startup-side-menu.component.spec.ts @@ -0,0 +1,133 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { StartupSideMenuComponent } from './startup-side-menu.component'; +import { By } from '@angular/platform-browser'; + +describe('StartupSideMenuComponent', () => { + let component: StartupSideMenuComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StartupSideMenuComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(StartupSideMenuComponent); + component = fixture.componentInstance; + + // Set required inputs + fixture.componentRef.setInput('version', 'V1.0.0'); + fixture.componentRef.setInput('links', [ + { + icon: 'description', + label: 'Documentation', + url: 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector#doc', + }, + { + icon: 'bug_report', + label: 'Report Bug', + url: 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector/issues', + }, + { + icon: 'code', + label: 'GitHub', + url: 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector#github', + }, + ]); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the version', () => { + const versionEl = fixture.debugElement.query( + By.css('.version'), + ).nativeElement; + expect(versionEl.textContent).toContain('V1.0.0'); + }); + + it('should emit newInvestigation when New Inspection button is clicked', () => { + const emitSpy = spyOn(component.newInvestigation, 'emit'); + + // Find the button with text "New Inspection" + const buttons = fixture.debugElement.queryAll( + By.css('button[mat-flat-button]'), + ); + const btn = buttons.find((b) => + b.nativeElement.textContent.includes('New Inspection'), + ); + + expect(btn).toBeTruthy(); + btn!.nativeElement.click(); + + expect(emitSpy).toHaveBeenCalled(); + }); + + it('should emit openKhiFile when Open .khi file button is clicked', () => { + const emitSpy = spyOn(component.openKhiFile, 'emit'); + + // Find the button with text "Open .khi file" + const buttons = fixture.debugElement.queryAll( + By.css('button[mat-stroked-button]'), + ); + const btn = buttons.find((b) => + b.nativeElement.textContent.includes('Open .khi file'), + ); + + expect(btn).toBeTruthy(); + btn!.nativeElement.click(); + + expect(emitSpy).toHaveBeenCalled(); + }); + + it('should have correct external links', () => { + const links = fixture.debugElement.queryAll(By.css('.footer-link')); + expect(links.length).toBe(3); + + const docLink = links.find((l) => + l.nativeElement.textContent.includes('Documentation'), + ); + expect(docLink).toBeTruthy(); + expect(docLink!.nativeElement.getAttribute('href')).toBe( + 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector#doc', + ); + expect(docLink!.nativeElement.getAttribute('target')).toBe('_blank'); + expect(docLink!.nativeElement.getAttribute('rel')).toBe( + 'noopener noreferrer', + ); + + const bugLink = links.find((l) => + l.nativeElement.textContent.includes('Report Bug'), + ); + expect(bugLink).toBeTruthy(); + expect(bugLink!.nativeElement.getAttribute('href')).toBe( + 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector/issues', + ); + + const githubLink = links.find((l) => + l.nativeElement.textContent.includes('GitHub'), + ); + expect(githubLink).toBeTruthy(); + expect(githubLink!.nativeElement.getAttribute('href')).toBe( + 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector#github', + ); + }); +}); diff --git a/web/src/app/dialogs/startup/components/startup-side-menu.component.ts b/web/src/app/dialogs/startup/components/startup-side-menu.component.ts new file mode 100644 index 00000000..9a0b6e99 --- /dev/null +++ b/web/src/app/dialogs/startup/components/startup-side-menu.component.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, input, output } from '@angular/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { KHIIconRegistrationModule } from 'src/app/shared/module/icon-registration.module'; +import { SidebarLink } from '../types/startup-side-menu.types'; + +/** + * Sidebar component for the startup dialog. + * Displays the application logo, title, main action buttons, and footer links. + */ +@Component({ + selector: 'khi-startup-side-menu', + imports: [ + MatTooltipModule, + MatIconModule, + MatButtonModule, + KHIIconRegistrationModule, + ], + templateUrl: './startup-side-menu.component.html', + styleUrls: ['./startup-side-menu.component.scss'], +}) +export class StartupSideMenuComponent { + /** The current version of the application to be displayed. */ + public readonly version = input.required(); + + /** The list of links to be displayed in the footer. */ + public readonly links = input.required(); + + /** Emitted when the user clicks the 'New Investigation' button. */ + public readonly newInvestigation = output(); + + /** Emitted when the user clicks the 'Open .khi file' button. */ + public readonly openKhiFile = output(); +} diff --git a/web/src/app/dialogs/startup/components/startup-side-menu.stories.ts b/web/src/app/dialogs/startup/components/startup-side-menu.stories.ts new file mode 100644 index 00000000..74337c69 --- /dev/null +++ b/web/src/app/dialogs/startup/components/startup-side-menu.stories.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Meta, StoryObj } from '@storybook/angular'; +import { StartupSideMenuComponent } from './startup-side-menu.component'; + +const meta: Meta = { + title: 'Dialogs/Startup/StartupSideMenu', + component: StartupSideMenuComponent, + tags: ['autodocs'], + argTypes: { + newInvestigation: { action: 'newInvestigation' }, + openKhiFile: { action: 'openKhiFile' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + version: 'v0.100.21', + links: [ + { + icon: 'description', + label: 'Documentation', + url: 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector', + }, + { + icon: 'bug_report', + label: 'Report Bug', + url: 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector/issues', + }, + { + icon: 'code', + label: 'GitHub', + url: 'https://github.com/GoogleCloudPlatform/kubernetes-history-inspector', + }, + ], + }, +}; diff --git a/web/src/app/dialogs/startup/types/startup-side-menu.types.ts b/web/src/app/dialogs/startup/types/startup-side-menu.types.ts new file mode 100644 index 00000000..f6a4256c --- /dev/null +++ b/web/src/app/dialogs/startup/types/startup-side-menu.types.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Represents a link to be displayed in the sidebar footer. + */ +export interface SidebarLink { + /** The name of the Material icon to display. */ + icon: string; + /** The label text to display. */ + label: string; + /** The URL destination of the link. */ + url: string; +}