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

NAS-128557 / 24.10 / Finishing WidgetGroupFormComponent #10022

Merged
merged 68 commits into from May 14, 2024
Merged

Conversation

RehanY147
Copy link
Contributor

No description provided.

@RehanY147 RehanY147 requested a review from a team as a code owner May 3, 2024 03:44
@RehanY147 RehanY147 requested review from bvasilenko and removed request for a team May 3, 2024 03:44
@bugclerk
Copy link
Contributor

bugclerk commented May 3, 2024

@bugclerk bugclerk changed the title Finishing WidgetGroupFormComponent NAS-128557 / 24.10 / Finishing WidgetGroupFormComponent May 3, 2024
Copy link

codecov bot commented May 3, 2024

Codecov Report

Attention: Patch coverage is 74.16974% with 70 lines in your changes are missing coverage. Please review.

Project coverage is 4.86%. Comparing base (b918e55) to head (0d55def).
Report is 2 commits behind head on master.

Files Patch % Lines
...roup-slot-form/widget-group-slot-form.component.ts 83.47% 19 Missing ⚠️
...settings/widget-interface-ip-settings.component.ts 28.00% 18 Missing ⚠️
...ges/dashboard/types/form-widget-group.interface.ts 0.00% 13 Missing ⚠️
...s/widget-group-form/widget-group-form.component.ts 92.85% 6 Missing ⚠️
...s/dashboard/types/widget-settings-ref.interface.ts 40.00% 6 Missing ⚠️
...pp/modules/ix-forms/utils/get-form-errors.utils.ts 16.66% 5 Missing ⚠️
...components/configuration/configuration.elements.ts 0.00% 2 Missing ⚠️
...dget-interface-ip/widget-interface-ip.component.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #10022       +/-   ##
===========================================
- Coverage   73.86%    4.86%   -69.01%     
===========================================
  Files        1520     1526        +6     
  Lines       53202    53447      +245     
  Branches     6352     6387       +35     
===========================================
- Hits        39296     2598    -36698     
- Misses      13906    50849    +36943     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@undsoft undsoft self-requested a review May 3, 2024 07:25
Copy link
Collaborator

@undsoft undsoft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs more work.

[disabled]="(widgetGroupFormStore.hasValidationErrors$ | async)"
(click)="onSubmit()"
>{{ 'Save' | translate }}</button>
</ix-form-actions>

<!-- TODO: Category dropdown, pull widget definitions from widgetRegistry and regroup by category -->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the todos now.

this.setCategoryOptions();
}

getEnumKeyByEnumValue<T extends Record<string, string>>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will lead to untranslatable categories. You should just use widgetCategoryLabels.

},
});
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of this design.
You're mixing two types of communication. You have both communication through inputs and through the store. This also makes settings component know about things like store and slotIndex, when it doesn't really need to know this. Inheritance also makes it harder to navigate the code.

I'd rather we communicate via inputs and outputs here without additional dependencies and an abstract class.
If there is an issue with outputs in componentOutlet, use https://angular.io/api/core/ViewContainerRef#createComponent or pass-in a subject.


@UntilDestroy()
@Directive()
export abstract class WidgetSettingsDirective<Settings extends SomeWidgetSettings = null> implements OnInit {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can still be called WidgetSettingsComponent.

export class WidgetGroupFormStore extends ComponentStore<WidgetGroupFormState> {
readonly layout$ = this.select((state) => state.layout);
readonly hasValidationErrors$ = this.select((state) => {
for (const slot of state.slots) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return state.slots.some((slot) => slot.validationErrors);

};
});

getSlotConfig(slotIndex: number): SlotConfig {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused.

});

this.widgetTypesOptions$ = of(sizeSuitedTypes.map((type) => {
return { label: this.getEnumKeyByEnumValue(WidgetType, type), value: type };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name should come from widgetDefinition and be translated.

return keys.length > 0 ? keys[0] : null;
}

setCategoryOptions(): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is complicated and you are doing similar things here and in setWidgetTypeOptions.
Instead, why not first filter for widgets that fit the slot size in a private method:

    const widgets = Object.values(widgetRegistry);
    const layout = this.layout.value;
    const slotSize = layoutToSlotSizes[layout][this.selectedSlot];

    const supportedWidgets = widgets.filter((widget) => widget.supportedSizes.includes(slotSize));

and then extract categories:

    const categories = uniq(supportedWidgets.map((widget) => widget.category));
    return categories.map((category) => {
      return {
        label: widgetCategoryLabels.get(category) || category,
        value: category
      };
    });

or types when you need them.


// TODO: Implement template options
templateOptions$ = of([]);

constructor(
private formBuilder: FormBuilder,
private chainedRef: ChainedRef<WidgetGroup>,
protected chainedRef: ChainedRef<WidgetGroup>,
Copy link
Collaborator

@undsoft undsoft May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Editing existing widget groups should be supported.

});
readonly layoutsMap = widgetGroupIcons;

protected readonly template = new FormControl('');
Copy link
Collaborator

@undsoft undsoft May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think component store makes your life harder here, because instead of having one source of truth you have things stored in three places. There are form controls, there's group inside of the component and there's a store.
It's really hard to trace what changes what here.

Having a separate place for logic is a good idea, but then we should come up with a way to utilize it more.

We should avoid doing what we did in pool creation wizard, because it really makes for some complicated logic.

Options for dropdowns should react to changes in data store whereever it is.

See this video https://www.youtube.com/watch?v=skOTEbGwncE
It focuses on rxjs, but you could do the same thing easier with signals.

You need to pick one place for source of truth (could be a store, could be a form or signals here or in a service) and then have other things derive from it.

@undsoft
Copy link
Collaborator

undsoft commented May 3, 2024

Also you are storing validation errors for every slot, which is fine, but then we need to come up with an error state for those widgets, so that user knows where the error is if he switches to another slot.

@RehanY147
Copy link
Contributor Author

Will add tests once the approach is approved.

@RehanY147 RehanY147 requested a review from undsoft May 6, 2024 06:25
Copy link
Collaborator

@undsoft undsoft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Practical issues:

  1. I see null widget is not supported, when I should just see Empty and it shouldn't be white.
  2. When I update settings, they should update the preview.
  3. We need to clean up widget settings when user presses save.

@@ -75,7 +75,7 @@ export class DashboardComponent implements OnInit {
protected onAddGroup(): void {
const newGroup: WidgetGroup = {
layout: WidgetGroupLayout.Full,
slots: [],
slots: [{ category: null, type: null }],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a behaviour specific to WidgetGroupFormComponent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, we should be adding a new group here at all. But we're doing that and we are adding the new group to the dashboard right here. But since, we're doing that, a full layout widget has at least 1 slot and the object should represent that. I will remove the whole logic from here.

});
readonly layoutsMap = widgetGroupIcons;

get hasErrors(): boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a computed signal.


setTypeOptions(category: WidgetCategory): void {
this.form.controls.type.setValue(null);
const layoutSupportedWidgets = this.getLayoutSupportedWidgets() as Widget[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as Widget[] is incorrect here.
You could try introducing an UntypedWidgetDefinition similar to WidgetDefinition, but without generics to return in getLayoutSupportedWidgets.
Or if you only care about smaller set of fields, you can introduce a new interface with just those fields locally.

this.refreshSettingsContainer();
}

setCategoryOptions(): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance of turning some of those methods that set options into computed signals?

@@ -13,6 +16,13 @@ export enum WidgetType {
Cpu = 'cpu',
}

export const widgetTypeLabels = new Map<WidgetType, string>([
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use name field in widgetDefinition is for this.

@@ -21,6 +31,7 @@ export enum SlotSize {

export interface Widget {
type: WidgetType;
category: WidgetCategory;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be part of Widget interface. We are storing widget settings in user attributes on middleware and we shouldn't store category there, because there is no need to tightly couple the connection there.

type WidgetSettingsComponentType<Settings> = Settings extends SomeWidgetSettings
? WidgetSettingsComponent<Settings>
? Settings
Copy link
Collaborator

@undsoft undsoft May 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original intent was to tie widget settings to widgets, so that developers don't make mistakes when they define their components.
This is now in broken state.

You need to:

  1. Make WidgetSettingsRef accept a Settings generic.
  2. Recover back WidgetSettingsComponent and have it require WidgetSettingsRef<Settings> as one of the fields (instead of something).
  3. Have WidgetInterfaceIpSettingsComponent implement WidgetSettingsComponent correctly.

import { ValidationErrors } from '@angular/forms';

export class WidgetSettingsRef {
getData: () => { slot: number; settings: object };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really see why settings components need to know what slot number they are in.
You can add slot number to WidgetSettingsRef in a way that would hide this information or you can just rely on currently selected slot.

});
}

private getAllFormErrors(): Record<string, ValidationErrors> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to go to some utils function or be part of WidgetSettingsRef class.

}

getLayoutSupportedWidgets(): { type: WidgetType; category: WidgetCategory; [key: string]: unknown }[] {
const widgetsEntires = Object.entries(widgetRegistry);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

@RehanY147 RehanY147 requested a review from undsoft May 10, 2024 12:27
Copy link
Collaborator

@undsoft undsoft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👍🏼

@RehanY147 RehanY147 enabled auto-merge (squash) May 10, 2024 23:14
@undsoft undsoft disabled auto-merge May 11, 2024 08:16
@undsoft
Copy link
Collaborator

undsoft commented May 11, 2024

Looked into failing tests a little bit.
I think this kind of error usually happens when there is an unexpected network request, but I cannot be sure.
The easiest current way to diagnose this is to update package.json to only run a few tests like so:

"test:pr": "yarn run check-env -s && echo 'Setting up temporary environment file...\\n' && yarn run ui remote -i 'headless.local' && jest --coverage --maxWorkers=2 src/app/pages/dashboard/components",

and then to start removing parts of template / component to see where the issue is.

My suspicion is that MatIconTestingModule doesn't stop network requests completely or that there is an issue with another dependency.

Also checks layout selector doesn't check anything. It should at least validate that changing the layout changes the preview by doing something like:

      const editor = spectator.query(WidgetEditorGroupComponent);
      expect(editor.group).toMatchObject(...);

It would be good to update other tests to also test what user sees / actual behaviour of the component, instead of calling component functions that should be marked as protected.

@RehanY147 RehanY147 force-pushed the NAS-128557 branch 2 times, most recently from 29ce6b1 to fad2f76 Compare May 14, 2024 08:36
@RehanY147 RehanY147 merged commit c174c12 into master May 14, 2024
10 checks passed
@RehanY147 RehanY147 deleted the NAS-128557 branch May 14, 2024 10:33
@bugclerk
Copy link
Contributor

This PR has been merged and conversations have been locked.
If you would like to discuss more about this issue please use our forums or raise a Jira ticket.

@truenas truenas locked as resolved and limited conversation to collaborators May 14, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
3 participants