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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#7](https://github.com/kobsio/kobs/pull/7): Share datasource options between components and allow sharing of URLs.
- [#11](https://github.com/kobsio/kobs/pull/11): :warning: *Breaking change:* :warning: Refactor cluster and application handling.
- [#17](https://github.com/kobsio/kobs/pull/17): Use location to load applications, which allows user to share their applications view.
- [#20](https://github.com/kobsio/kobs/pull/20): Rework usage of icons, links handling and drawer layout.
4 changes: 1 addition & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,11 @@ export default <PLUGIN-NAME>Page;

```tsx
import React from 'react';
import ListIcon from '@patternfly/react-icons/dist/js/icons/list-icon';

import { IPluginProps } from 'utils/plugins';
import PluginDataMissing from 'components/plugins/PluginDataMissing';

const <PLUGIN-NAME>Plugin: React.FunctionComponent<IPluginProps> = ({
isInDrawer,
name,
description,
plugin,
Expand All @@ -261,7 +259,7 @@ const <PLUGIN-NAME>Plugin: React.FunctionComponent<IPluginProps> = ({
title="<PLUGIN-NAME> properties are missing"
description="The <PLUGIN-NAME> properties are missing in your CR for this application. Visit the documentation to learn more on how to use the <PLUGIN-NAME> plugin in an Application CR."
documentation="https://kobs.io"
icon={ListIcon}
type="<PLUGIN-TYPE>"
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"@patternfly/patternfly": "^4.80.3",
"@patternfly/react-charts": "^6.14.2",
"@patternfly/react-core": "^4.90.2",
"@patternfly/react-icons": "^4.9.5",
"@patternfly/react-table": "^4.20.15",
"@types/google-protobuf": "^3.7.4",
"@types/node": "^14.14.35",
Expand Down
24 changes: 24 additions & 0 deletions app/src/components/DrawerLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Button, ButtonVariant } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { Link } from 'react-router-dom';
import React from 'react';

interface IDrawerLinkProps {
link: string;
icon?: React.ReactNode;
}

// DrawerLink is a component, which can be used to render an additional link in the Drawer header next to the
// DrawerCloseButton component. The component requires a link and an optional icon. If no icon is specified the
// ExternalLinkAltIcon icon is used.
const DrawerLink: React.FunctionComponent<IDrawerLinkProps> = ({ link, icon }: IDrawerLinkProps) => {
return (
<div className="pf-c-drawer__close">
<Link to={link}>
<Button variant={ButtonVariant.plain}>{icon ? icon : <ExternalLinkAltIcon />}</Button>
</Link>
</div>
);
};

export default DrawerLink;
29 changes: 11 additions & 18 deletions app/src/components/HomeItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core';
import React from 'react';
import { useHistory } from 'react-router-dom';

import LinkWrapper from 'components/LinkWrapper';

// IHomeItemProps is the interface for an item on the home page. Each item contains a title, body, link and icon.
interface IHomeItemProps {
Expand All @@ -13,24 +14,16 @@ interface IHomeItemProps {
// HomeItem is used to render an item in the home page. It requires a title, body, link and icon. When the card is
// clicked, the user is redirected to the provided link.
const HomeItem: React.FunctionComponent<IHomeItemProps> = ({ body, link, title, icon }: IHomeItemProps) => {
const history = useHistory();

const handleClick = (): void => {
if (link.startsWith('http')) {
window.open(link, '_blank');
} else {
history.push(link);
}
};

return (
<Card style={{ cursor: 'pointer' }} isHoverable={true} onClick={handleClick}>
<CardHeader>
<img src={icon} alt={title} width="27px" style={{ marginRight: '5px' }} />
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardBody>{body}</CardBody>
</Card>
<LinkWrapper link={link}>
<Card style={{ cursor: 'pointer' }} isHoverable={true}>
<CardHeader>
<img src={icon} alt={title} width="27px" style={{ marginRight: '5px' }} />
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardBody>{body}</CardBody>
</Card>
</LinkWrapper>
);
};

Expand Down
24 changes: 24 additions & 0 deletions app/src/components/LinkWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Link } from 'react-router-dom';
import React from 'react';

interface ILinkWrapperProps {
link: string;
children: React.ReactElement;
}

// LinkWrapper component can be used to wrap another component inside a link. This can be used for selectedable cards,
// which should navigate the user to another location. This is to prefer over an onClick handler, so that the user can
// decide if he wants the link in a new tab or not.
const LinkWrapper: React.FunctionComponent<ILinkWrapperProps> = ({ children, link }: ILinkWrapperProps) => {
return (
<Link
to={link}
target={link.startsWith('http') ? '_blank' : undefined}
style={{ color: 'inherit', textDecoration: 'inherit' }}
>
{children}
</Link>
);
};

export default LinkWrapper;
6 changes: 3 additions & 3 deletions app/src/components/applications/ApplicationDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const ApplicationDetails: React.FunctionComponent<IApplicationDetailsProps> = ({
<DrawerHead>
<Title title={application.name} subtitle={`${application.namespace} (${application.cluster})`} size="lg" />
<DrawerActions style={{ padding: 0 }}>
<ApplicationDetailsLink application={application} />
<DrawerCloseButton onClose={close} />
</DrawerActions>
</DrawerHead>
Expand All @@ -59,9 +60,6 @@ const ApplicationDetails: React.FunctionComponent<IApplicationDetailsProps> = ({
<p>{application.details.description}</p>

<List variant={ListVariant.inline}>
<ListItem>
<ApplicationDetailsLink application={application} />
</ListItem>
{application.details.linksList.map((link, index) => (
<ListItem key={index}>
<a href={link.link} rel="noreferrer" target="_blank">
Expand Down Expand Up @@ -89,6 +87,8 @@ const ApplicationDetails: React.FunctionComponent<IApplicationDetailsProps> = ({
refResourcesContent={refResourcesContent}
refPluginsContent={refPluginsContent}
/>

<p>&nbsp;</p>
</DrawerPanelBody>
</DrawerPanelContent>
);
Expand Down
5 changes: 3 additions & 2 deletions app/src/components/applications/ApplicationDetailsLink.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Link, useLocation } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';

import { Application } from 'proto/application_pb';
import DrawerLink from 'components/DrawerLink';

interface IApplicationDetailsLinkProps {
application: Application.AsObject;
Expand All @@ -23,7 +24,7 @@ const ApplicationDetailsLink: React.FunctionComponent<IApplicationDetailsLinkPro
setLink(`/applications/${application.cluster}/${application.namespace}/${application.name}${location.search}`);
}, [application, location.search]);

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

export default ApplicationDetailsLink;
16 changes: 8 additions & 8 deletions app/src/components/applications/ApplicationTabsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const ApplicationTabsContent: React.FunctionComponent<IApplicationTabsContent> =

const pageSection = (
<PageSection
style={isInDrawer ? { minHeight: '100%', paddingLeft: '0px', paddingRight: '0px' } : { minHeight: '100%' }}
style={{ minHeight: '100%' }}
variant={isInDrawer ? PageSectionVariants.light : PageSectionVariants.default}
>
<TabContent
Expand All @@ -61,10 +61,13 @@ const ApplicationTabsContent: React.FunctionComponent<IApplicationTabsContent> =
namespaces: [application.namespace],
resources: application.resourcesList,
}}
selectResource={(resource: IRow): void =>
selectResource={
isInDrawer
? setPanelContent(undefined)
: setPanelContent(<ResourceDetails resource={resource} close={(): void => setPanelContent(undefined)} />)
? undefined
: (resource: IRow): void =>
setPanelContent(
<ResourceDetails resource={resource} close={(): void => setPanelContent(undefined)} />,
)
}
/>
</TabContent>
Expand All @@ -82,11 +85,8 @@ const ApplicationTabsContent: React.FunctionComponent<IApplicationTabsContent> =
<div>
{mountedTabs[`refPlugin-${index}`] ? (
<Plugin
isInDrawer={isInDrawer}
plugin={plugin}
showDetails={(details: React.ReactNode): void =>
isInDrawer ? setPanelContent(undefined) : setPanelContent(details)
}
showDetails={isInDrawer ? undefined : (details: React.ReactNode): void => setPanelContent(details)}
/>
) : null}
</div>
Expand Down
3 changes: 1 addition & 2 deletions app/src/components/applications/ApplicationsToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import {
ToolbarItem,
ToolbarToggleGroup,
} from '@patternfly/react-core';
import { FilterIcon, SearchIcon } from '@patternfly/react-icons';
import React, { useContext, useState } from 'react';
import FilterIcon from '@patternfly/react-icons/dist/js/icons/filter-icon';
import SearchIcon from '@patternfly/react-icons/dist/js/icons/search-icon';

import { ClustersContext, IClusterContext } from 'context/ClustersContext';
import ToolbarItemClusters from 'components/resources/ToolbarItemClusters';
Expand Down
15 changes: 4 additions & 11 deletions app/src/components/plugins/Plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@ import { Plugin as IPlugin } from 'proto/plugins_grpc_web_pb';
import { plugins } from 'utils/plugins';

interface IPluginProps {
isInDrawer: boolean;
plugin: IPlugin.AsObject;
showDetails: (panelContent: React.ReactNode) => void;
showDetails?: (panelContent: React.ReactNode) => void;
}

const Plugin: React.FunctionComponent<IPluginProps> = ({ isInDrawer, plugin, showDetails }: IPluginProps) => {
const Plugin: React.FunctionComponent<IPluginProps> = ({ plugin, showDetails }: IPluginProps) => {
const pluginsContext = useContext<IPluginsContext>(PluginsContext);
const pluginDetails = pluginsContext.getPluginDetails(plugin.name);

if (!pluginDetails || !plugins.hasOwnProperty(pluginDetails.type)) {
return (
<Alert variant={AlertVariant.danger} isInline={isInDrawer} title="Plugin was not found">
<Alert variant={AlertVariant.danger} title="Plugin was not found">
{pluginDetails ? (
<p>
The plugin <b>{plugin.name}</b> has an invalide type.
Expand All @@ -34,13 +33,7 @@ const Plugin: React.FunctionComponent<IPluginProps> = ({ isInDrawer, plugin, sho
const Component = plugins[pluginDetails.type].plugin;

return (
<Component
isInDrawer={isInDrawer}
name={plugin.name}
description={pluginDetails.description}
plugin={plugin}
showDetails={showDetails}
/>
<Component name={plugin.name} description={pluginDetails.description} plugin={plugin} showDetails={showDetails} />
);
};

Expand Down
72 changes: 37 additions & 35 deletions app/src/components/resources/ResourcesList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Accordion, AccordionContent, AccordionItem, AccordionToggle } from '@patternfly/react-core';
import { Accordion, AccordionContent, AccordionItem, AccordionToggle, Card } from '@patternfly/react-core';
import React, { useContext, useState } from 'react';
import { IRow } from '@patternfly/react-table';

Expand All @@ -8,7 +8,7 @@ import ResourcesListItem from 'components/resources/ResourcesListItem';

interface IResourcesListProps {
resources: IResources;
selectResource: (resource: IRow) => void;
selectResource?: (resource: IRow) => void;
}

// ResourcesList is a list of resources. The resources are displayed in an accordion view.
Expand All @@ -28,39 +28,41 @@ const ResourcesList: React.FunctionComponent<IResourcesListProps> = ({
};

return (
<Accordion asDefinitionList={false}>
{resources.resources.map((resource, i) => (
<div key={i}>
{resource.kindsList.map((kind, j) => (
<AccordionItem key={j}>
<AccordionToggle
onClick={(): void => toggle(`resources-accordion-${i}-${j}`)}
isExpanded={expanded.includes(`resources-accordion-${i}-${j}`)}
id={`resources-toggle-${i}-${j}`}
>
{clustersContext.resources ? clustersContext.resources[kind].title : ''}
</AccordionToggle>
<AccordionContent
id={`resources-content-${i}-${j}`}
style={{ maxWidth: '100%', overflowX: 'scroll' }}
isHidden={!expanded.includes(`resources-accordion-${i}-${j}`)}
isFixed={false}
>
{clustersContext.resources ? (
<ResourcesListItem
clusters={resources.clusters}
namespaces={resources.namespaces}
resource={clustersContext.resources[kind]}
selector={resource.selector}
selectResource={selectResource}
/>
) : null}
</AccordionContent>
</AccordionItem>
))}
</div>
))}
</Accordion>
<Card>
<Accordion asDefinitionList={false}>
{resources.resources.map((resource, i) => (
<div key={i}>
{resource.kindsList.map((kind, j) => (
<AccordionItem key={j}>
<AccordionToggle
onClick={(): void => toggle(`resources-accordion-${i}-${j}`)}
isExpanded={expanded.includes(`resources-accordion-${i}-${j}`)}
id={`resources-toggle-${i}-${j}`}
>
{clustersContext.resources ? clustersContext.resources[kind].title : ''}
</AccordionToggle>
<AccordionContent
id={`resources-content-${i}-${j}`}
style={{ maxWidth: '100%', overflowX: 'scroll' }}
isHidden={!expanded.includes(`resources-accordion-${i}-${j}`)}
isFixed={false}
>
{clustersContext.resources ? (
<ResourcesListItem
clusters={resources.clusters}
namespaces={resources.namespaces}
resource={clustersContext.resources[kind]}
selector={resource.selector}
selectResource={selectResource}
/>
) : null}
</AccordionContent>
</AccordionItem>
))}
</div>
))}
</Accordion>
</Card>
);
};

Expand Down
4 changes: 2 additions & 2 deletions app/src/components/resources/ResourcesListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface IResourcesListItemProps {
namespaces: string[];
resource: IResource;
selector: string;
selectResource: (resource: IRow) => void;
selectResource?: (resource: IRow) => void;
}

// ResourcesListItem is the table for a single resource. To get the resources the component needs a list of clusters,
Expand Down Expand Up @@ -72,7 +72,7 @@ const ResourcesListItem: React.FunctionComponent<IResourcesListItemProps> = ({
rows={rows}
>
<TableHeader />
<TableBody onRowClick={(e, row, props, data): void => selectResource(row)} />
<TableBody onRowClick={selectResource ? (e, row, props, data): void => selectResource(row) : undefined} />
</Table>
);
};
Expand Down
3 changes: 1 addition & 2 deletions app/src/components/resources/ResourcesToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import {
ToolbarItem,
ToolbarToggleGroup,
} from '@patternfly/react-core';
import { FilterIcon, SearchIcon } from '@patternfly/react-icons';
import React, { useContext, useState } from 'react';
import FilterIcon from '@patternfly/react-icons/dist/js/icons/filter-icon';
import SearchIcon from '@patternfly/react-icons/dist/js/icons/search-icon';

import { ClustersContext, IClusterContext } from 'context/ClustersContext';
import { IResources } from 'components/resources/Resources';
Expand Down
Loading