-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
refactor(ui): InputFilter and WorkflowTimeline components from class to functional #11899
Conversation
Signed-off-by: tetora1053 <[email protected]>
Signed-off-by: tetora1053 <[email protected]>
Signed-off-by: tetora1053 <[email protected]>
interface InputState { | ||
value: string; | ||
localCache: string[]; | ||
error?: Error; |
There was a problem hiding this comment.
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 !== '')); |
There was a problem hiding this comment.
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 😟
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
let resizeSubscription: Subscription; | ||
let refreshSubscription: Subscription; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resizeSubscription = fromEvent(window, 'resize').subscribe(updateWidth); | |
resizeSubscription.current = fromEvent(window, 'resize').subscribe(updateWidth); |
will need a .current
when it's converted to a ref
return () => { | ||
if (resizeSubscription) { | ||
resizeSubscription.unsubscribe(); | ||
} | ||
if (refreshSubscription) { | ||
refreshSubscription.unsubscribe(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
if (!refreshSubscription && !isCompleted) { | ||
refreshSubscription = interval(1000).subscribe(() => { | ||
setNow(moment()); | ||
}); | ||
if (nodes.length === 0) { | ||
return <div />; | ||
} else if (refreshSubscription && isCompleted) { | ||
refreshSubscription.unsubscribe(); | ||
refreshSubscription = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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!
Signed-off-by: tetora1053 <[email protected]>
There was a problem hiding this 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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Partial fix for #9810
Motivation
Modifications
this.state
->useState
this.props
->props
useRef
to manage references to DOM elementsVerification
InputFilter
WorkflowTimeline
Reference PRs