rfellows commented on code in PR #9221:
URL: https://github.com/apache/nifi/pull/9221#discussion_r1738886313


##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.html:
##########
@@ -0,0 +1,49 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<div class="content-viewer-background h-screen flex flex-col gap-y-4 p-4">
+    <div class="flex justify-between items-center">
+        <div>
+            <img ngSrc="assets/icons/nifi-logo-about.svg" priority width="150" 
height="64" alt="NiFi Logo" />
+        </div>
+        <form [formGroup]="viewerForm">
+            <mat-form-field subscriptSizing="dynamic" class="w-72">
+                <mat-label>View</mat-label>
+                <mat-select formControlName="viewAs" 
(selectionChange)="viewAsChanged($event)">
+                    @for (groupOption of viewAsOptions; track 
groupOption.text) {
+                        @if (groupOption.options.length > 1) {
+                            <mat-optgroup [label]="groupOption.text">
+                                @for (option of groupOption.options; track 
option.value) {
+                                    <mat-option [value]="option.value">{{ 
option.text }}</mat-option>
+                                }
+                            </mat-optgroup>
+                        } @else {
+                            <mat-option 
[value]="groupOption.options[0].value">{{ groupOption.text }}</mat-option>
+                        }
+                    }
+                </mat-select>
+            </mat-form-field>
+        </form>
+    </div>
+    <div class="p-2 flex-1 flex border overflow-y-auto">
+        @if (viewerSelected) {
+            <router-outlet></router-outlet>
+        } @else {
+            <div class="unset">No data reference specified</div>

Review Comment:
   There are 2 scenarios where this message is displayed.
   * there is no ref query param provided
   * the provided mime type is not supported by any content viewer
   
   In the first case, this message makes sense. However, in the second, 
   this phrase seems too vague to be useful to the user. I would think it 
should say something more to the effect "No compatible content viewer found for 
the mime type <requested_mime_type>."



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.html:
##########


Review Comment:
   Previously when viewing content the user was shown the Filename and mime 
type (referred to as Content Type) of what is being viewed. Is there a reason 
those are not displayed in the new iteration?



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.html:
##########
@@ -0,0 +1,49 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<div class="content-viewer-background h-screen flex flex-col gap-y-4 p-4">
+    <div class="flex justify-between items-center">
+        <div>
+            <img ngSrc="assets/icons/nifi-logo-about.svg" priority width="150" 
height="64" alt="NiFi Logo" />
+        </div>
+        <form [formGroup]="viewerForm">
+            <mat-form-field subscriptSizing="dynamic" class="w-72">
+                <mat-label>View</mat-label>
+                <mat-select formControlName="viewAs" 
(selectionChange)="viewAsChanged($event)">

Review Comment:
   Consider conditionally setting the panelWidth of this select between `auto` 
and `null` depending on if there are any types of viewers with multiple options.
   > If set to auto, the panel will match the trigger width. If set to null or 
an empty string, the panel will grow to match the longest option's text.
   
   If the case where there are multiple content viewers for the same display 
name, the displayed value is really long and wraps. It makes it challenging to 
read.
   
   <img width="321" alt="Screenshot 2024-08-30 at 1 34 41 PM" 
src="https://github.com/user-attachments/assets/c2378e36-b586-471f-885e-7eb2d976a0b6";>
   
   



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.ts:
##########
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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, OnDestroy, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../state';
+import { loadContentViewerOptions, resetContentViewerOptions } from 
'../state/viewer-options/viewer-options.actions';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { selectBundledViewerOptions, selectViewerOptions } from 
'../state/viewer-options/viewer-options.selectors';
+import { ContentViewer, HEX_VIEWER_URL, SupportedMimeTypes } from 
'../state/viewer-options';
+import { isDefinedAndNotNull, SelectGroup, SelectOption, selectQueryParams } 
from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { concatLatestFrom } from '@ngrx/operators';
+import { navigateToBundledContentViewer, resetContent, setRef } from 
'../state/content/content.actions';
+import { MatSelectChange } from '@angular/material/select';
+import { loadAbout } from '../../../state/about/about.actions';
+import { selectAbout } from '../../../state/about/about.selectors';
+import { filter, map, switchMap, take } from 'rxjs';
+import { navigateToExternalViewer } from 
'../state/external-viewer/external-viewer.actions';
+
+@Component({
+    selector: 'content-viewer',
+    templateUrl: './content-viewer.component.html',
+    styleUrls: ['./content-viewer.component.scss']
+})
+export class ContentViewerComponent implements OnInit, OnDestroy {
+    viewerForm: FormGroup;
+    viewAsOptions: SelectGroup[] = [];
+
+    private supportedMimeTypeId = 0;
+    private supportedMimeTypeLookup: Map<number, SupportedMimeTypes> = new 
Map<number, SupportedMimeTypes>();
+    private supportedMimeTypeContentViewerLookup: Map<number, ContentViewer> = 
new Map<number, ContentViewer>();
+    private mimeTypeDisplayNameLookup: Map<number, string> = new Map<number, 
string>();
+    private mimeTypeIdsSupportedByBundledUis: Set<number> = new Set<number>();
+
+    private defaultSupportedMimeTypeId: number | null = null;
+
+    viewerSelected: boolean = false;
+    private mimeType: string | undefined;
+    private clientId: string | undefined;
+
+    private queryParamsLoaded = false;
+    private viewerOptionsLoaded = false;
+
+    constructor(
+        private store: Store<NiFiState>,
+        private formBuilder: FormBuilder
+    ) {
+        this.viewerForm = this.formBuilder.group({ viewAs: null });
+
+        this.store
+            .select(selectViewerOptions)
+            .pipe(
+                isDefinedAndNotNull(),
+                concatLatestFrom(() => 
this.store.select(selectBundledViewerOptions)),
+                takeUntilDestroyed()
+            )
+            .subscribe(([externalViewerOptions, bundledViewerOptions]) => {
+                this.supportedMimeTypeLookup.clear();
+                this.supportedMimeTypeContentViewerLookup.clear();
+                this.mimeTypeIdsSupportedByBundledUis.clear();
+                this.mimeTypeDisplayNameLookup.clear();
+
+                // maps a given content (by display name) to the supported 
mime type id
+                // which can be used to look up the corresponding content 
viewer
+                const supportedMimeTypeMapping = new Map<string, number[]>();
+
+                // process all external viewer options
+                externalViewerOptions.forEach((contentViewer) => {
+                    
contentViewer.supportedMimeTypes.forEach((supportedMimeType) => {
+                        const supportedMimeTypeId = this.supportedMimeTypeId++;
+
+                        if 
(!supportedMimeTypeMapping.has(supportedMimeType.displayName)) {
+                            
supportedMimeTypeMapping.set(supportedMimeType.displayName, []);
+                        }
+                        
supportedMimeTypeMapping.get(supportedMimeType.displayName)?.push(supportedMimeTypeId);
+
+                        this.supportedMimeTypeLookup.set(supportedMimeTypeId, 
supportedMimeType);
+                        
this.supportedMimeTypeContentViewerLookup.set(supportedMimeTypeId, 
contentViewer);
+                    });
+                });
+
+                // process all bundled options
+                bundledViewerOptions.forEach((contentViewer) => {
+                    
contentViewer.supportedMimeTypes.forEach((supportedMimeType) => {
+                        const supportedMimeTypeId = this.supportedMimeTypeId++;
+
+                        if (contentViewer.uri === HEX_VIEWER_URL) {
+                            this.defaultSupportedMimeTypeId = 
supportedMimeTypeId;
+                        }
+
+                        if 
(!supportedMimeTypeMapping.has(supportedMimeType.displayName)) {
+                            
supportedMimeTypeMapping.set(supportedMimeType.displayName, []);
+                        }
+                        
supportedMimeTypeMapping.get(supportedMimeType.displayName)?.push(supportedMimeTypeId);
+
+                        
this.mimeTypeIdsSupportedByBundledUis.add(supportedMimeTypeId);
+                        this.supportedMimeTypeLookup.set(supportedMimeTypeId, 
supportedMimeType);
+                        
this.supportedMimeTypeContentViewerLookup.set(supportedMimeTypeId, 
contentViewer);
+                    });
+                });
+
+                const newViewAsOptions: SelectGroup[] = [];
+                supportedMimeTypeMapping.forEach((contentViewers, displayName) 
=> {
+                    const options: SelectOption[] = [];
+                    contentViewers.forEach((contentViewerId) => {
+                        this.mimeTypeDisplayNameLookup.set(contentViewerId, 
displayName);
+
+                        const contentViewer = 
this.supportedMimeTypeContentViewerLookup.get(contentViewerId);
+                        if (contentViewer) {
+                            const option: SelectOption = {
+                                text: contentViewer.displayName,
+                                value: String(contentViewerId)
+                            };
+                            options.push(option);
+                        }
+                    });
+                    const groupOption: SelectGroup = {
+                        text: displayName,
+                        options
+                    };
+                    newViewAsOptions.push(groupOption);
+                });
+
+                this.viewAsOptions = newViewAsOptions;

Review Comment:
   can we sort this list by display name or something? the ordering that it 
comes out in now feels random.



##########
nifi-frontend/src/main/frontend/apps/standard-content-viewer/src/app/pages/standard-content-viewer/feature/standard-content-viewer.component.ts:
##########
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { StandardContentViewerState } from '../../../state';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { isDefinedAndNotNull, selectQueryParams } from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { ContentViewerService } from '../service/content-viewer.service';
+import { HttpErrorResponse } from '@angular/common/http';
+
+@Component({
+    selector: 'standard-content-viewer',
+    templateUrl: './standard-content-viewer.component.html',
+    styleUrls: ['./standard-content-viewer.component.scss']
+})
+export class StandardContentViewer {
+    contentFormGroup: FormGroup;
+
+    private mode = 'text/plain';
+    private ref: string | null = null;
+    private clientId: string | undefined = undefined;
+
+    mimeTypeDisplayName: string | null = null;
+    error: string | null = null;
+    contentLoaded = false;
+
+    constructor(
+        private formBuilder: FormBuilder,
+        private store: Store<StandardContentViewerState>,
+        private contentViewerService: ContentViewerService
+    ) {
+        this.contentFormGroup = this.formBuilder.group({
+            value: '',
+            formatted: 'true'
+        });
+
+        this.store
+            .select(selectQueryParams)
+            .pipe(isDefinedAndNotNull(), takeUntilDestroyed())
+            .subscribe((queryParams) => {
+                const dataRef: string | undefined = queryParams['ref'];
+                const mimeTypeDisplayName: string | undefined = 
queryParams['mimeTypeDisplayName'];
+                if (dataRef && mimeTypeDisplayName) {
+                    this.ref = dataRef;
+                    this.mimeTypeDisplayName = mimeTypeDisplayName;
+                    this.clientId = queryParams['clientId'];
+
+                    this.loadContent();
+                }
+            });
+    }
+
+    loadContent(): void {
+        if (this.ref && this.mimeTypeDisplayName) {
+            this.setMode(this.mimeTypeDisplayName);
+
+            const formatted: string = 
this.contentFormGroup.get('formatted')?.value;
+            this.contentViewerService
+                .getContent(this.ref, this.mimeTypeDisplayName, formatted, 
this.clientId)
+                .subscribe({
+                    error: (errorResponse: HttpErrorResponse) => {
+                        const errorBodyString = errorResponse.error;
+                        if (typeof errorBodyString === 'string') {
+                            try {
+                                const errorBody = JSON.parse(errorBodyString);
+                                this.error = errorBody.message;
+                            } catch (e) {
+                                this.error = 'Unable to load content.';
+                            }
+                        } else {
+                            this.error = 'Unable to load content.';
+                        }
+                        this.contentLoaded = true;
+
+                        this.contentFormGroup.get('value')?.setValue('');
+                    },
+                    next: (content) => {
+                        this.error = null;
+                        this.contentLoaded = true;
+
+                        this.contentFormGroup.get('value')?.setValue(content);
+                    }
+                });
+        }
+    }
+
+    private setMode(mimeTypeDisplayName: string): void {
+        switch (mimeTypeDisplayName) {
+            case 'json':
+            case 'avro':
+                this.mode = 'application/json';
+                break;
+            case 'xml':
+                this.mode = 'application/xml';
+                break;
+            case 'yaml':
+                this.mode = 'application/yaml';

Review Comment:
   ```suggestion
                   this.mode = 'text/x-yaml';
   ```
   
   using `text/x-yaml` when setting the mode for codemirror provides syntax 
highlighting.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.ts:
##########
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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, OnDestroy, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../state';
+import { loadContentViewerOptions, resetContentViewerOptions } from 
'../state/viewer-options/viewer-options.actions';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { selectBundledViewerOptions, selectViewerOptions } from 
'../state/viewer-options/viewer-options.selectors';
+import { ContentViewer, HEX_VIEWER_URL, SupportedMimeTypes } from 
'../state/viewer-options';
+import { isDefinedAndNotNull, SelectGroup, SelectOption, selectQueryParams } 
from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { concatLatestFrom } from '@ngrx/operators';
+import { navigateToBundledContentViewer, resetContent, setRef } from 
'../state/content/content.actions';
+import { MatSelectChange } from '@angular/material/select';
+import { loadAbout } from '../../../state/about/about.actions';
+import { selectAbout } from '../../../state/about/about.selectors';
+import { filter, map, switchMap, take } from 'rxjs';
+import { navigateToExternalViewer } from 
'../state/external-viewer/external-viewer.actions';
+
+@Component({
+    selector: 'content-viewer',
+    templateUrl: './content-viewer.component.html',
+    styleUrls: ['./content-viewer.component.scss']
+})
+export class ContentViewerComponent implements OnInit, OnDestroy {
+    viewerForm: FormGroup;
+    viewAsOptions: SelectGroup[] = [];
+
+    private supportedMimeTypeId = 0;
+    private supportedMimeTypeLookup: Map<number, SupportedMimeTypes> = new 
Map<number, SupportedMimeTypes>();
+    private supportedMimeTypeContentViewerLookup: Map<number, ContentViewer> = 
new Map<number, ContentViewer>();
+    private mimeTypeDisplayNameLookup: Map<number, string> = new Map<number, 
string>();
+    private mimeTypeIdsSupportedByBundledUis: Set<number> = new Set<number>();
+
+    private defaultSupportedMimeTypeId: number | null = null;
+
+    viewerSelected: boolean = false;
+    private mimeType: string | undefined;
+    private clientId: string | undefined;
+
+    private queryParamsLoaded = false;
+    private viewerOptionsLoaded = false;
+
+    constructor(
+        private store: Store<NiFiState>,
+        private formBuilder: FormBuilder
+    ) {
+        this.viewerForm = this.formBuilder.group({ viewAs: null });
+
+        this.store
+            .select(selectViewerOptions)
+            .pipe(
+                isDefinedAndNotNull(),
+                concatLatestFrom(() => 
this.store.select(selectBundledViewerOptions)),
+                takeUntilDestroyed()
+            )
+            .subscribe(([externalViewerOptions, bundledViewerOptions]) => {
+                this.supportedMimeTypeLookup.clear();
+                this.supportedMimeTypeContentViewerLookup.clear();
+                this.mimeTypeIdsSupportedByBundledUis.clear();
+                this.mimeTypeDisplayNameLookup.clear();
+
+                // maps a given content (by display name) to the supported 
mime type id
+                // which can be used to look up the corresponding content 
viewer
+                const supportedMimeTypeMapping = new Map<string, number[]>();
+
+                // process all external viewer options
+                externalViewerOptions.forEach((contentViewer) => {
+                    
contentViewer.supportedMimeTypes.forEach((supportedMimeType) => {
+                        const supportedMimeTypeId = this.supportedMimeTypeId++;
+
+                        if 
(!supportedMimeTypeMapping.has(supportedMimeType.displayName)) {
+                            
supportedMimeTypeMapping.set(supportedMimeType.displayName, []);
+                        }
+                        
supportedMimeTypeMapping.get(supportedMimeType.displayName)?.push(supportedMimeTypeId);
+
+                        this.supportedMimeTypeLookup.set(supportedMimeTypeId, 
supportedMimeType);
+                        
this.supportedMimeTypeContentViewerLookup.set(supportedMimeTypeId, 
contentViewer);
+                    });
+                });
+
+                // process all bundled options
+                bundledViewerOptions.forEach((contentViewer) => {
+                    
contentViewer.supportedMimeTypes.forEach((supportedMimeType) => {
+                        const supportedMimeTypeId = this.supportedMimeTypeId++;
+
+                        if (contentViewer.uri === HEX_VIEWER_URL) {
+                            this.defaultSupportedMimeTypeId = 
supportedMimeTypeId;
+                        }
+
+                        if 
(!supportedMimeTypeMapping.has(supportedMimeType.displayName)) {
+                            
supportedMimeTypeMapping.set(supportedMimeType.displayName, []);
+                        }
+                        
supportedMimeTypeMapping.get(supportedMimeType.displayName)?.push(supportedMimeTypeId);
+
+                        
this.mimeTypeIdsSupportedByBundledUis.add(supportedMimeTypeId);
+                        this.supportedMimeTypeLookup.set(supportedMimeTypeId, 
supportedMimeType);
+                        
this.supportedMimeTypeContentViewerLookup.set(supportedMimeTypeId, 
contentViewer);
+                    });
+                });
+
+                const newViewAsOptions: SelectGroup[] = [];
+                supportedMimeTypeMapping.forEach((contentViewers, displayName) 
=> {
+                    const options: SelectOption[] = [];
+                    contentViewers.forEach((contentViewerId) => {

Review Comment:
   the use of `contentViewerId` is a bit confusing to me. Everywhere else it is 
referred to as `supportedMimeTypeId`. At first I thought it was something 
different until I traced it back to how it was getting set.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.ts:
##########
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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, OnDestroy, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../state';
+import { loadContentViewerOptions, resetContentViewerOptions } from 
'../state/viewer-options/viewer-options.actions';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { selectBundledViewerOptions, selectViewerOptions } from 
'../state/viewer-options/viewer-options.selectors';
+import { ContentViewer, HEX_VIEWER_URL, SupportedMimeTypes } from 
'../state/viewer-options';
+import { isDefinedAndNotNull, SelectGroup, SelectOption, selectQueryParams } 
from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { concatLatestFrom } from '@ngrx/operators';
+import { navigateToBundledContentViewer, resetContent, setRef } from 
'../state/content/content.actions';
+import { MatSelectChange } from '@angular/material/select';
+import { loadAbout } from '../../../state/about/about.actions';
+import { selectAbout } from '../../../state/about/about.selectors';
+import { filter, map, switchMap, take } from 'rxjs';
+import { navigateToExternalViewer } from 
'../state/external-viewer/external-viewer.actions';
+
+@Component({
+    selector: 'content-viewer',
+    templateUrl: './content-viewer.component.html',
+    styleUrls: ['./content-viewer.component.scss']
+})
+export class ContentViewerComponent implements OnInit, OnDestroy {
+    viewerForm: FormGroup;
+    viewAsOptions: SelectGroup[] = [];
+
+    private supportedMimeTypeId = 0;
+    private supportedMimeTypeLookup: Map<number, SupportedMimeTypes> = new 
Map<number, SupportedMimeTypes>();
+    private supportedMimeTypeContentViewerLookup: Map<number, ContentViewer> = 
new Map<number, ContentViewer>();
+    private mimeTypeDisplayNameLookup: Map<number, string> = new Map<number, 
string>();
+    private mimeTypeIdsSupportedByBundledUis: Set<number> = new Set<number>();

Review Comment:
   These are all keyed off of the same value (the assigned mime type id). Seems 
like a single map of complex object would be a simpler way to approach this 
IMO. Thoughts on moving to something like:
   
   assuming some interface like so:
   ```
   interface SupportedContentViewer {
       supportedMimeTypes: SupportedMimeTypes;
       contentViewer: ContentViewer;
       displayName: string;
       supportedByBundleUis: boolean;
   }
   ```
   
   These could all be contained in a single lookup map:
   ```
   private supportedMimeTypeLookup: Map<number, SupportedContentViewer>
   ```
   



##########
nifi-frontend/src/main/frontend/apps/standard-content-viewer/src/app/pages/standard-content-viewer/feature/standard-content-viewer.component.ts:
##########
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { StandardContentViewerState } from '../../../state';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { isDefinedAndNotNull, selectQueryParams } from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { ContentViewerService } from '../service/content-viewer.service';
+import { HttpErrorResponse } from '@angular/common/http';
+
+@Component({
+    selector: 'standard-content-viewer',
+    templateUrl: './standard-content-viewer.component.html',
+    styleUrls: ['./standard-content-viewer.component.scss']
+})
+export class StandardContentViewer {
+    contentFormGroup: FormGroup;
+
+    private mode = 'text/plain';
+    private ref: string | null = null;
+    private clientId: string | undefined = undefined;
+
+    mimeTypeDisplayName: string | null = null;
+    error: string | null = null;
+    contentLoaded = false;
+
+    constructor(
+        private formBuilder: FormBuilder,
+        private store: Store<StandardContentViewerState>,
+        private contentViewerService: ContentViewerService
+    ) {
+        this.contentFormGroup = this.formBuilder.group({
+            value: '',
+            formatted: 'true'
+        });
+
+        this.store
+            .select(selectQueryParams)
+            .pipe(isDefinedAndNotNull(), takeUntilDestroyed())
+            .subscribe((queryParams) => {
+                const dataRef: string | undefined = queryParams['ref'];
+                const mimeTypeDisplayName: string | undefined = 
queryParams['mimeTypeDisplayName'];
+                if (dataRef && mimeTypeDisplayName) {
+                    this.ref = dataRef;
+                    this.mimeTypeDisplayName = mimeTypeDisplayName;
+                    this.clientId = queryParams['clientId'];
+
+                    this.loadContent();
+                }
+            });
+    }
+
+    loadContent(): void {
+        if (this.ref && this.mimeTypeDisplayName) {
+            this.setMode(this.mimeTypeDisplayName);
+

Review Comment:
   shouldn't `this.contentLoaded` be set to `false` here? If the backend takes 
a non-trivial amount of time to respond we would want the skeleton loaders to 
display, no? I simulated this by throwing a `delay(2000)` in the request to the 
`contentViewerService.getContent` below. Without setting the contentLoaded back 
to false prior to getting the new content, the UI appears to not respond until 
the results are back.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to