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

refactor(ui): InputFilter and WorkflowTimeline components from class to functional #11899

Conversation

tetora1053
Copy link
Contributor

@tetora1053 tetora1053 commented Sep 28, 2023

Partial fix for #9810

  • 15. ui/src/app/shared/components/input-filter.tsx:
  • 44. ui/src/app/workflows/components/workflow-timeline/workflow-timeline.tsx:

Motivation

  • Use newer, modern functional components compatible with hooks instead of legacy class-based components

Modifications

  • this.state -> useState
  • this.props -> props
  • useRef to manage references to DOM elements
  • methods -> inner functions

Verification

  • Mostly no real semantic changes
  • manually tested in local environment

InputFilter
Screen Shot 2023-09-29 at 1 37 06
Screen Shot 2023-09-29 at 1 37 20

WorkflowTimeline
Screen Shot 2023-09-29 at 1 41 20

Reference PRs

interface InputState {
value: string;
localCache: string[];
error?: Error;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

error state isn't used, so removed it.

Signed-off-by: tetora1053 <[email protected]>
}
export function InputFilter(props: InputProps) {
const [value, setValue] = useState(props.value);
const [localCache, setLocalCache] = useState((localStorage.getItem(props.name + '_inputs') || '').split(',').filter(item => item !== ''));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ts infers the type of localCache correctly, so I omitted type specification. (useState<string[]>)
but at the same time, I think it also looks good to specify the type for readability.
which should I choose 😟

Copy link
Contributor

Choose a reason for hiding this comment

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

yea same as your previous PR, I think when the inference is correct and straightforward (string in this case), there's no need to annotate it.

I also have experience working in ML-family languages like OCaml, which are even stronger typed and where the compiler infers the types of the entire program (so there are no type annotations at all). On the other spectrum would be something like Java, which requires on type annotations on almost everything and feels very verbose as a result (despite having weaker guarantees than OCaml et al).

For TS, I prefer to have strict typing on, which we'll be able to do in this codebase once there is some progress on decoupling argo-ui (see argoproj/argo-ui#453). With strict typing, if a type can't be correctly inferred, the compiler will tell you (right now I believe it only has a warning).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see. unless it's a complex type, I'll try to omit the type annotations where TS correctly infers. thanks for detailed description.😄

and I had no idea about argo-ui! Thank you for letting me know. I'm really hopeful for the progress in decoupling. It sounds it could bring great improvements.

@tetora1053 tetora1053 marked this pull request as ready for review September 28, 2023 17:36
@agilgur5 agilgur5 self-requested a review September 28, 2023 18:15
Copy link
Contributor

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

just a few changes needed to refs in the WorkflowTimeline component, otherwise the rest looks good 😄

this.refreshSubscription.unsubscribe();
this.refreshSubscription = null;
useEffect(() => {
ensureRunningWorkflowRefreshing(props.workflow);
Copy link
Contributor

Choose a reason for hiding this comment

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

we can in-line this function now as it's only used here now

Copy link
Contributor Author

@tetora1053 tetora1053 Sep 29, 2023

Choose a reason for hiding this comment

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

thanks!
I in-lined the function, and I noticedcompletedPhase variable is static, so I hoisted it up to the module level
https://github.com/argoproj/argo-workflows/pull/11899/files#diff-24ff460ef8551421b42892db8b66a3636c1e3797bceb65679d51fb1547aab43bR14

Comment on lines 22 to 23
let resizeSubscription: Subscription;
let refreshSubscription: Subscription;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let resizeSubscription: Subscription;
let refreshSubscription: Subscription;
const resizeSubscription = useRef<Subscription>(null);
const refreshSubscription = useRef<Subscription>(null);

these actually should be refs as well so that they are persisted between renders.
Right now I think it only still works by coincidence, since all the functions have it in their scope, but the best practice (and least error prone) here would be to use refs

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you also move these to be together with containerRef as they were before? so that all refs are together

this.state = {parentWidth: 0, now: moment()};
this.ensureRunningWorkflowRefreshing(props.workflow);
}
const containerRef = useRef(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const containerRef = useRef(null);
const containerRef = useRef<HTMLElement>(null);

this one does need a type annotation 😉
(since null can't be inferred to be an HTMLElement)

this.updateWidth();
}
useEffect(() => {
resizeSubscription = fromEvent(window, 'resize').subscribe(updateWidth);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
resizeSubscription = fromEvent(window, 'resize').subscribe(updateWidth);
resizeSubscription.current = fromEvent(window, 'resize').subscribe(updateWidth);

will need a .current when it's converted to a ref

Comment on lines 34 to 40
return () => {
if (resizeSubscription) {
resizeSubscription.unsubscribe();
}
if (refreshSubscription) {
refreshSubscription.unsubscribe();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return () => {
if (resizeSubscription) {
resizeSubscription.unsubscribe();
}
if (refreshSubscription) {
refreshSubscription.unsubscribe();
}
return () => {
resizeSubscription.current?.unsubscribe();
refreshSubscription.current?.unsubscribe();

can simplify these with optional chaining syntax

return diff;
function ensureRunningWorkflowRefreshing(workflow: models.Workflow) {
const completedPhases = [models.NODE_PHASE.ERROR, models.NODE_PHASE.SUCCEEDED, models.NODE_PHASE.SKIPPED, models.NODE_PHASE.OMITTED, models.NODE_PHASE.FAILED];
const isCompleted = workflow && workflow.status && completedPhases.indexOf(workflow.status.phase) > -1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const isCompleted = workflow && workflow.status && completedPhases.indexOf(workflow.status.phase) > -1;
const isCompleted = workflow && workflow.status && completedPhases.includes(workflow.status.phase);

can use the newer includes function now

Comment on lines 57 to 63
if (!refreshSubscription && !isCompleted) {
refreshSubscription = interval(1000).subscribe(() => {
setNow(moment());
});
if (nodes.length === 0) {
return <div />;
} else if (refreshSubscription && isCompleted) {
refreshSubscription.unsubscribe();
refreshSubscription = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (!refreshSubscription && !isCompleted) {
refreshSubscription = interval(1000).subscribe(() => {
setNow(moment());
});
if (nodes.length === 0) {
return <div />;
} else if (refreshSubscription && isCompleted) {
refreshSubscription.unsubscribe();
refreshSubscription = null;
if (!refreshSubscription.current && !isCompleted) {
refreshSubscription.current = interval(1000).subscribe(() => {
setNow(moment());
});
} else if (refreshSubscription.current && isCompleted) {
refreshSubscription.current.unsubscribe();
refreshSubscription.current = null;

will need to add .current here when it's a ref as well

Copy link
Contributor Author

@tetora1053 tetora1053 Sep 29, 2023

Choose a reason for hiding this comment

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

while making the modifications, I was wondering, is the line refreshSubscription.current = null; necessary?
wouldn't refreshSubscription.current.unsubscribe(); take care of it? 🤔

};
function setValueAndCache(newValue: string) {
setLocalCache(currentCache => {
const updatedCache = [...currentCache];
Copy link
Contributor

Choose a reason for hiding this comment

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

nice catch to make this an immutable operation!

Copy link
Contributor

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for updating this and fixing some subtle bugs (like the mutable updates) in the process too!

Copy link
Member

@terrytangyuan terrytangyuan left a comment

Choose a reason for hiding this comment

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

Thanks!

@terrytangyuan terrytangyuan enabled auto-merge (squash) September 30, 2023 02:00
@terrytangyuan terrytangyuan merged commit 68ad039 into argoproj:master Sep 30, 2023
25 checks passed
@tetora1053 tetora1053 deleted the refactor-ui-components-from-class-to-functional branch September 30, 2023 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants