-
Notifications
You must be signed in to change notification settings - Fork 5.8k
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
[Dashboard] Add task detail page with logs #35328
Changes from all commits
2ff46de
e376e61
d39423b
356315a
c47df84
387f71f
aa3779f
f828790
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,9 +25,7 @@ const useStyles = makeStyles((theme) => | |
|
||
export type MultiTabLogViewerTabDetails = { | ||
title: string; | ||
nodeId: string | null; | ||
filename?: string; | ||
}; | ||
} & LogViewerData; | ||
|
||
export type MultiTabLogViewerProps = { | ||
tabs: MultiTabLogViewerTabDetails[]; | ||
|
@@ -45,6 +43,10 @@ export const MultiTabLogViewer = ({ | |
|
||
const currentTab = tabs.find((tab) => tab.title === value); | ||
|
||
if (tabs.length === 0) { | ||
return <Typography>No logs to display.</Typography>; | ||
} | ||
|
||
return ( | ||
<div className={className}> | ||
<Box | ||
|
@@ -59,52 +61,56 @@ export const MultiTabLogViewer = ({ | |
alignItems="stretch" | ||
flexGrow={1} | ||
> | ||
<Tabs | ||
className={classes.tabs} | ||
value={value} | ||
onChange={(_, newValue) => { | ||
setValue(newValue); | ||
}} | ||
indicatorColor="primary" | ||
> | ||
{tabs.map(({ title }) => ( | ||
<Tab key={title} label={title} value={title} /> | ||
))} | ||
{otherLogsLink && ( | ||
<Tab | ||
label={ | ||
<Box display="flex" alignItems="center"> | ||
Other logs <RiExternalLinkLine size={20} /> | ||
</Box> | ||
} | ||
onClick={(event) => { | ||
// Prevent the tab from changing | ||
setValue(value); | ||
}} | ||
component={Link} | ||
to={otherLogsLink} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
/> | ||
)} | ||
</Tabs> | ||
{(tabs.length > 1 || otherLogsLink) && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you have a screenshot of what the page looks like with only one tab? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
<Tabs | ||
className={classes.tabs} | ||
value={value} | ||
onChange={(_, newValue) => { | ||
setValue(newValue); | ||
}} | ||
indicatorColor="primary" | ||
> | ||
{tabs.map(({ title }) => ( | ||
<Tab key={title} label={title} value={title} /> | ||
))} | ||
{otherLogsLink && ( | ||
<Tab | ||
label={ | ||
<Box display="flex" alignItems="center"> | ||
Other logs <RiExternalLinkLine size={20} /> | ||
</Box> | ||
} | ||
onClick={(event) => { | ||
// Prevent the tab from changing | ||
setValue(value); | ||
}} | ||
component={Link} | ||
to={otherLogsLink} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
/> | ||
)} | ||
</Tabs> | ||
)} | ||
|
||
{!currentTab ? ( | ||
<Typography color="error">Please select a tab.</Typography> | ||
) : ( | ||
tabs.map(({ title, nodeId, filename }) => ( | ||
<HideableBlock | ||
key={title} | ||
visible={title === currentTab?.title} | ||
keepRendered | ||
> | ||
<StateApiLogViewer | ||
nodeId={nodeId} | ||
filename={filename} | ||
height={expanded ? 800 : 300} | ||
/> | ||
</HideableBlock> | ||
)) | ||
tabs.map((tab) => { | ||
const { title, ...data } = tab; | ||
return ( | ||
<HideableBlock | ||
key={title} | ||
visible={title === currentTab?.title} | ||
keepRendered | ||
> | ||
<StateApiLogViewer | ||
data={data} | ||
height={expanded ? 800 : 300} | ||
/> | ||
</HideableBlock> | ||
); | ||
}) | ||
)} | ||
</Box> | ||
<IconButton | ||
|
@@ -119,23 +125,114 @@ export const MultiTabLogViewer = ({ | |
); | ||
}; | ||
|
||
export type StateApiLogViewerProps = { | ||
nodeId?: string | null; | ||
type TextData = { | ||
contents: string; | ||
}; | ||
type FileData = { | ||
nodeId: string | null; | ||
filename?: string; | ||
}; | ||
type ActorData = { | ||
actorId: string | null; | ||
suffix: "out" | "err"; | ||
}; | ||
type TaskData = { | ||
taskId: string | null; | ||
suffix: "out" | "err"; | ||
}; | ||
|
||
type LogViewerData = TextData | FileData | ActorData | TaskData; | ||
|
||
const isLogViewerDataText = (data: LogViewerData): data is TextData => | ||
"contents" in data; | ||
|
||
const isLogViewerDataActor = (data: LogViewerData): data is ActorData => | ||
"actorId" in data; | ||
|
||
const isLogViewerDataTask = (data: LogViewerData): data is TaskData => | ||
"taskId" in data; | ||
|
||
export type StateApiLogViewerProps = { | ||
height?: number; | ||
data: LogViewerData; | ||
}; | ||
|
||
export const StateApiLogViewer = ({ | ||
height = 300, | ||
data, | ||
}: StateApiLogViewerProps) => { | ||
if (isLogViewerDataText(data)) { | ||
return <TextLogViewer height={height} contents={data.contents} />; | ||
} else if (isLogViewerDataActor(data)) { | ||
return <ActorLogViewer height={height} {...data} />; | ||
} else if (isLogViewerDataTask(data)) { | ||
return <TaskLogViewer height={height} {...data} />; | ||
} else { | ||
return <FileLogViewer height={height} {...data} />; | ||
} | ||
}; | ||
|
||
const TextLogViewer = ({ | ||
height = 300, | ||
contents, | ||
}: { | ||
height: number; | ||
contents: string; | ||
}) => { | ||
return <LogViewer log={contents} height={height} />; | ||
}; | ||
|
||
const FileLogViewer = ({ | ||
height = 300, | ||
nodeId, | ||
filename, | ||
}: { | ||
height: number; | ||
} & FileData) => { | ||
const apiData = useStateApiLogs({ nodeId, filename }, filename); | ||
return <ApiLogViewer apiData={apiData} height={height} />; | ||
}; | ||
|
||
const ActorLogViewer = ({ | ||
height = 300, | ||
}: StateApiLogViewerProps) => { | ||
const { downloadUrl, log, path, refresh } = useStateApiLogs(nodeId, filename); | ||
actorId, | ||
suffix, | ||
}: { | ||
height: number; | ||
} & ActorData) => { | ||
const apiData = useStateApiLogs( | ||
{ actorId, suffix }, | ||
`actor-log-${actorId}.${suffix}`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems like passing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what do you mean by not used for filtering? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. disregard the earlier message, is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't that clear by the type definition?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would still add the comment. If someone were to look at the base |
||
); | ||
return <ApiLogViewer apiData={apiData} height={height} />; | ||
}; | ||
|
||
const TaskLogViewer = ({ | ||
height = 300, | ||
taskId, | ||
suffix, | ||
}: { | ||
height: number; | ||
} & TaskData) => { | ||
const apiData = useStateApiLogs( | ||
{ taskId, suffix }, | ||
`task-log-${taskId}.${suffix}`, | ||
); | ||
return <ApiLogViewer apiData={apiData} height={height} />; | ||
}; | ||
|
||
const ApiLogViewer = ({ | ||
apiData: { downloadUrl, log, path, refresh }, | ||
height = 300, | ||
}: { | ||
apiData: ReturnType<typeof useStateApiLogs>; | ||
height: number; | ||
}) => { | ||
return typeof log === "string" ? ( | ||
<LogViewer | ||
log={log} | ||
path={path} | ||
downloadUrl={downloadUrl} | ||
downloadUrl={downloadUrl !== null ? downloadUrl : undefined} | ||
height={height} | ||
onRefreshClick={() => { | ||
refresh(); | ||
|
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.
it's unclear how this works - wouldn't you need to specify the children/descendant routes here or does this somehow route to the
ActorDetailLayout
path below?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 don't quite understand the question. The ActorDetailPage is a child of this ActorDetailLayout
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.
ah I misread the code, the
Layout
is wrapped so ActorDetail and Task are children, makes sense