Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#1](https://github.com/kobsio/kobs/pull/1): Fix mobile layout for the cluster and namespace filter by using a Toolbar instead of FlexItems.

### Changed

- [#7](https://github.com/kobsio/kobs/pull/7): Share datasource options between components and allow sharing of URLs.
50 changes: 15 additions & 35 deletions app/src/components/applications/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,15 @@ import {
ListVariant,
PageSection,
PageSectionVariants,
Tab,
TabContent,
TabTitleText,
Tabs,
} from '@patternfly/react-core';
import { Link, useHistory, useParams } from 'react-router-dom';
import React, { useCallback, useEffect, useRef, useState } from 'react';

import { GetApplicationRequest, GetApplicationResponse } from 'generated/proto/clusters_pb';
import Tabs, { DEFAULT_TAB } from 'components/applications/details/Tabs';
import { Application } from 'generated/proto/application_pb';
import { ClustersPromiseClient } from 'generated/proto/clusters_grpc_web_pb';
import Metrics from 'components/applications/details/metrics/Metrics';
import Resources from 'components/applications/details/resources/Resources';
import TabsContent from 'components/applications/details/TabsContent';
import Title from 'components/shared/Title';
import { apiURL } from 'utils/constants';

Expand All @@ -39,7 +35,8 @@ const Applications: React.FunctionComponent = () => {
const params = useParams<IApplicationsParams>();
const [application, setApplication] = useState<Application | undefined>(undefined);
const [error, setError] = useState<string>('');
const [activeTabKey, setActiveTabKey] = useState<string>('resources');

const [tab, setTab] = useState<string>(DEFAULT_TAB);
const refResourcesContent = useRef<HTMLElement>(null);
const refMetricsContent = useRef<HTMLElement>(null);

Expand Down Expand Up @@ -116,37 +113,20 @@ const Applications: React.FunctionComponent = () => {
))}
</List>
<Tabs
className="pf-u-mt-md"
mountOnEnter={true}
isFilled={true}
activeKey={activeTabKey}
onSelect={(event, tabIndex): void => setActiveTabKey(tabIndex.toString())}
>
<Tab
eventKey="resources"
title={<TabTitleText>Resources</TabTitleText>}
tabContentId="refResources"
tabContentRef={refResourcesContent}
/>
<Tab
eventKey="metrics"
title={<TabTitleText>Metrics</TabTitleText>}
tabContentId="refMetrics"
tabContentRef={refMetricsContent}
/>
</Tabs>
tab={tab}
setTab={(t: string): void => setTab(t)}
refResourcesContent={refResourcesContent}
refMetricsContent={refMetricsContent}
/>
</PageSection>

<PageSection variant={PageSectionVariants.default}>
<TabContent eventKey="resources" id="refResources" ref={refResourcesContent} aria-label="Resources">
<div>
<Resources application={application} />
</div>
</TabContent>
<TabContent eventKey="metrics" id="refMetrics" ref={refMetricsContent} aria-label="Metrics">
{/* We have to check if the refMetricsContent is not null, because otherwise the Metrics component will be shown below the resources component. */}
<div>{refMetricsContent.current ? <Metrics application={application} /> : null}</div>
</TabContent>
<TabsContent
application={application}
tab={tab}
refResourcesContent={refResourcesContent}
refMetricsContent={refMetricsContent}
/>
</PageSection>
</React.Fragment>
);
Expand Down
31 changes: 31 additions & 0 deletions app/src/components/applications/details/DetailsLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Link, useLocation } from 'react-router-dom';
import React, { useEffect, useState } from 'react';

import { Application } from 'generated/proto/application_pb';

interface IDetailsLinkProps {
application: Application;
}

// DetailsLink renders the link to the details page for an application inside the DrawerPanel of the applications page.
// Everytime when the location.search parameter (query parameters) are changing, we are adding the new parameters to the
// link, so that for example a change of the selected time range is also used in the details page.
const DetailsLink: React.FunctionComponent<IDetailsLinkProps> = ({ application }: IDetailsLinkProps) => {
const location = useLocation();

const [link, setLink] = useState<string>(
`/applications/${application.getCluster()}/${application.getNamespace()}/${application.getName()}`,
);

useEffect(() => {
setLink(
`/applications/${application.getCluster()}/${application.getNamespace()}/${application.getName()}${
location.search
}`,
);
}, [application, location.search]);

return <Link to={link}>Details</Link>;
};

export default DetailsLink;
48 changes: 20 additions & 28 deletions app/src/components/applications/details/DrawerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ import {
List,
ListItem,
ListVariant,
Tab,
TabTitleText,
Tabs,
} from '@patternfly/react-core';
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { Link } from 'react-router-dom';

import Tabs, { DEFAULT_TAB } from 'components/applications/details/Tabs';
import { Application } from 'generated/proto/application_pb';
import Metrics from 'components/applications/details/metrics/Metrics';
import Resources from 'components/applications/details/resources/Resources';
import DetailsLink from 'components/applications/details/DetailsLink';
import TabsContent from 'components/applications/details/TabsContent';
import Title from 'components/shared/Title';

interface IDrawerPanelProps {
Expand All @@ -27,7 +25,9 @@ interface IDrawerPanelProps {
// DrawerPanel is the drawer panel for an application. It is used to display application details in the applications
// page. The details contains information for resources, metrics, logs and traces.
const DrawerPanel: React.FunctionComponent<IDrawerPanelProps> = ({ application, close }: IDrawerPanelProps) => {
const [activeTabKey, setActiveTabKey] = useState<string>('resources');
const [tab, setTab] = useState<string>(DEFAULT_TAB);
const refResourcesContent = useRef<HTMLElement>(null);
const refMetricsContent = useRef<HTMLElement>(null);

return (
<DrawerPanelContent minSize="50%">
Expand All @@ -45,11 +45,7 @@ const DrawerPanel: React.FunctionComponent<IDrawerPanelProps> = ({ application,
<DrawerPanelBody className="kobs-drawer-panel-body">
<List variant={ListVariant.inline}>
<ListItem>
<Link
to={`/applications/${application.getCluster()}/${application.getNamespace()}/${application.getName()}`}
>
Details
</Link>
<DetailsLink application={application} />
</ListItem>
{application.getLinksList().map((link, index) => (
<ListItem key={index}>
Expand All @@ -61,22 +57,18 @@ const DrawerPanel: React.FunctionComponent<IDrawerPanelProps> = ({ application,
</List>

<Tabs
mountOnEnter={true}
isFilled={true}
activeKey={activeTabKey}
onSelect={(event, tabIndex): void => setActiveTabKey(tabIndex.toString())}
>
<Tab eventKey="resources" title={<TabTitleText>Resources</TabTitleText>}>
<div>
<Resources application={application} />
</div>
</Tab>
<Tab eventKey="metrics" title={<TabTitleText>Metrics</TabTitleText>}>
<div>
<Metrics application={application} />
</div>
</Tab>
</Tabs>
tab={tab}
setTab={(t: string): void => setTab(t)}
refResourcesContent={refResourcesContent}
refMetricsContent={refMetricsContent}
/>

<TabsContent
application={application}
tab={tab}
refResourcesContent={refResourcesContent}
refMetricsContent={refMetricsContent}
/>
</DrawerPanelBody>
</DrawerPanelContent>
);
Expand Down
47 changes: 47 additions & 0 deletions app/src/components/applications/details/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Tabs as PatternflyTabs, Tab, TabTitleText } from '@patternfly/react-core';
import React from 'react';

// DEFAULT_TAB is the first tab, which is selected in the application view.
export const DEFAULT_TAB = 'resources';

interface ITabsParams {
tab: string;
setTab(tab: string): void;
refResourcesContent: React.RefObject<HTMLElement>;
refMetricsContent: React.RefObject<HTMLElement>;
}

// Tabs renders the tabs header, which are used by the user to select a section he wants to view for an application.
// We can not use the tab state, within this component, because then the tab change isn't reflected in the TabsContent
// component. So that we have to manage the refs and tab in the parent component.
const Tabs: React.FunctionComponent<ITabsParams> = ({
tab,
setTab,
refResourcesContent,
refMetricsContent,
}: ITabsParams) => {
return (
<PatternflyTabs
className="pf-u-mt-md"
mountOnEnter={true}
isFilled={true}
activeKey={tab}
onSelect={(event, tabIndex): void => setTab(tabIndex.toString())}
>
<Tab
eventKey="resources"
title={<TabTitleText>Resources</TabTitleText>}
tabContentId="refResources"
tabContentRef={refResourcesContent}
/>
<Tab
eventKey="metrics"
title={<TabTitleText>Metrics</TabTitleText>}
tabContentId="refMetrics"
tabContentRef={refMetricsContent}
/>
</PatternflyTabs>
);
};

export default Tabs;
122 changes: 122 additions & 0 deletions app/src/components/applications/details/TabsContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { TabContent } from '@patternfly/react-core';

import { Application } from 'generated/proto/application_pb';
import { IDatasourceOptions } from 'utils/proto';
import Metrics from 'components/applications/details/metrics/Metrics';
import Resources from 'components/applications/details/resources/Resources';

// IParsedDatasourceOptions is the interface for the parsed query parameters. It must contain the same keys as the
// IDatasourceOptions options, but all keys must be of type string.
interface IParsedDatasourceOptions {
resolution: string;
timeEnd: string;
timeStart: string;
}

// datasourceOptionsFromLocationSearch is used to parse all query parameters during the first rendering of the
// TabsContent component. When the parameters are not set we return some default options for the datasources. Because it
// could happen that only some parameters are set via location.search, we have to check each property if it contains a
// valid value. If this is the case we are overwriting the default value.
const datasourceOptionsFromLocationSearch = (): IDatasourceOptions => {
const search = window.location.search;
const options: IDatasourceOptions = {
resolution: '',
timeEnd: Math.floor(Date.now() / 1000),
timeStart: Math.floor(Date.now() / 1000) - 3600,
};

if (search !== '') {
try {
const parsedOptions: IParsedDatasourceOptions = JSON.parse(
'{"' + search.substr(1).replace(/&/g, '", "').replace(/=/g, '": "') + '"}',
);

if (parsedOptions.resolution) options.resolution = parsedOptions.resolution;
if (parsedOptions.timeEnd) options.timeEnd = parseInt(parsedOptions.timeEnd);
if (parsedOptions.timeStart) options.timeStart = parseInt(parsedOptions.timeStart);
} catch (err) {
return options;
}
}

return options;
};

// createSearch creates a string, which can be used within the history.push function as search parameter. For that we
// are looping over each key of the IDatasourceOptions interface and if it contains a value, this value will be added to
// the parameters.
const createSearch = (options: IDatasourceOptions): string => {
const params: string[] = [];

let option: keyof IDatasourceOptions;
for (option in options) {
if (options[option]) {
params.push(`${option}=${options[option]}`);
}
}

return `?${params.join('&')}`;
};

interface ITabsContent {
application: Application;
tab: string;
refResourcesContent: React.RefObject<HTMLElement>;
refMetricsContent: React.RefObject<HTMLElement>;
}

// TabsContent renders the content for a selected tab from the Tabs component. We also manage the datasource options,
// within this component, so that we can share the selected time range between metrics, logs and traces.
// When the datasource options are changed, we also reflect this change in the URL via query parameters, so that a user
// can share his current view with other users.
const TabsContent: React.FunctionComponent<ITabsContent> = ({
application,
tab,
refResourcesContent,
refMetricsContent,
}: ITabsContent) => {
const history = useHistory();
const location = useLocation();
const [datasourceOptions, setDatasourceOptions] = useState<IDatasourceOptions>(datasourceOptionsFromLocationSearch());

const changeDatasourceOptions = (options: IDatasourceOptions): void => {
setDatasourceOptions(options);

history.push({
pathname: location.pathname,
search: createSearch(options),
});
};

return (
<React.Fragment>
<TabContent
eventKey="resources"
id="refResources"
activeKey={tab}
ref={refResourcesContent}
aria-label="Resources"
>
<div>
<Resources application={application} />
</div>
</TabContent>
<TabContent eventKey="metrics" id="refMetrics" activeKey={tab} ref={refMetricsContent} aria-label="Metrics">
{/* We have to check if the refMetricsContent is not null, because otherwise the Metrics component will be shown below the resources component. */}
<div>
{refMetricsContent.current ? (
<Metrics
datasourceOptions={datasourceOptions}
setDatasourceOptions={changeDatasourceOptions}
application={application}
/>
) : null}
</div>
</TabContent>
</React.Fragment>
);
};

export default TabsContent;
Loading