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
4 changes: 3 additions & 1 deletion frontend/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"eslint.enable": true,

"prettier.eslintIntegration": true,
"javascript.validate.enable": false
"javascript.validate.enable": false,
"debug.node.autoAttach": "on",
"typescript.tsdk": "node_modules/typescript/lib"
Copy link
Contributor

Choose a reason for hiding this comment

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

👍


// TODO support prettier + stylelint
// "prettier.stylelintIntegration": true,
Expand Down
7 changes: 6 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@
"brace": "0.11.x",
"classnames": "2.x",
"core-js": "2.x",
"d3": "^5.9.2",
"file-saver": "1.3.x",
"font-awesome": "4.7.x",
"formik": "2.0.1-rc.1",
Copy link
Member

Choose a reason for hiding this comment

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

We're locking to a prerelease version?

Copy link
Contributor

Choose a reason for hiding this comment

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

@spadgett We are using V2 because it comes with support for custom formik hooks and it makes creating custom components wrapped around formik context much easier. They're actually at 2.0.1-rc..5 but because of a bug related to validations i had to downgrade to rc-1. I'll keep track of it and update the version to latest as soon as there's fix.

"fuzzysearch": "1.0.x",
"history": "4.x",
"immutable": "3.x",
Expand All @@ -95,6 +97,7 @@
"react-jsonschema-form": "^1.0.4",
"react-lightweight-tooltip": "1.x",
"react-linkify": "^0.2.2",
"react-measure": "^2.2.6",
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI, I remember @spadgett and @rawagner discussing that this dependency isn't really necessary (I think the context was dashboard card grid layout).

Copy link
Contributor Author

@christianvogt christianvogt Jun 14, 2019

Choose a reason for hiding this comment

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

Hmmm what's the alternative; ref-width-hook? I suppose I could use this in conjunction with getting the client height manually from the parent node.

Copy link
Contributor

Choose a reason for hiding this comment

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

ping @rawagner

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, we used ref-width-hook instead of react-measure as width was enough for us

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm OK with adding react-measure if we really need it.

"react-modal": "3.x",
"react-redux": "5.x",
"react-router-dom": "4.3.x",
Expand All @@ -111,7 +114,8 @@
"url-polyfill": "^1.1.5",
"url-search-params-polyfill": "2.x",
"whatwg-fetch": "2.x",
"xterm": "^3.12.2"
"xterm": "^3.12.2",
"yup": "^0.27.0"
},
"devDependencies": {
"@types/classnames": "^2.2.7",
Expand Down Expand Up @@ -156,6 +160,7 @@
"protractor-fail-fast": "3.x",
"protractor-jasmine2-screenshot-reporter": "0.5.x",
"read-pkg": "5.x",
"redux-mock-store": "^1.5.3",
"resolve-url-loader": "2.x",
"sass-loader": "6.x",
"thread-loader": "1.x",
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/console-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test": "yarn --cwd ../.. run test packages/console-app"
},
"dependencies": {
"@console/dev-console": "0.0.0-fixed",
"@console/internal": "0.0.0-fixed",
"@console/plugin-sdk": "0.0.0-fixed",
"@console/shared": "0.0.0-fixed"
Expand Down
4 changes: 2 additions & 2 deletions frontend/packages/console-app/src/plugin.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { CogIcon } from '@patternfly/react-icons';
import { CogsIcon } from '@patternfly/react-icons';
import { Plugin, Perspective } from '@console/plugin-sdk';

type ConsumedExtensions = Perspective;
Expand All @@ -10,7 +10,7 @@ const plugin: Plugin<ConsumedExtensions> = [
properties: {
id: 'admin',
name: 'Administrator',
icon: <CogIcon />,
icon: <CogsIcon />,
landingPageURL: '/',
default: true,
},
Expand Down
22 changes: 11 additions & 11 deletions frontend/packages/console-plugin-sdk/src/typings/pages.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';
import { RouteProps, RouteComponentProps } from 'react-router';
import { RouteProps, RouteComponentProps } from 'react-router-dom';
import { K8sKind, K8sResourceKindReference } from '@console/internal/module/k8s';
import { Extension } from './extension';

type LazyLoader<T> = () => Promise<React.ComponentType<T>>;
type LazyLoader<T extends {}> = () => Promise<React.ComponentType<Partial<T>>>;

namespace ExtensionProperties {
export interface ResourcePage<T> {
Expand All @@ -15,26 +15,26 @@ namespace ExtensionProperties {

export type ResourceListPage = ResourcePage<{
/** See https://reacttraining.com/react-router/web/api/match */
match?: RouteComponentProps['match'];
match: RouteComponentProps['match'];
/** The resource kind scope. */
kind?: K8sResourceKindReference;
kind: K8sResourceKindReference;
/** Whether the page should assign focus when loaded. */
autoFocus?: boolean;
autoFocus: boolean;
/** Whether the page should mock the UI empty state. */
mock?: boolean;
mock: boolean;
/** The namespace scope. */
namespace?: string;
namespace: string;
}>;

export type ResourceDetailsPage = ResourcePage<{
/** See https://reacttraining.com/react-router/web/api/match */
match?: RouteComponentProps['match'];
match: RouteComponentProps['match'];
/** The resource kind scope. */
kind?: K8sResourceKindReference;
kind: K8sResourceKindReference;
/** The namespace scope. */
namespace?: string;
namespace: string;
/** The page name. */
name?: string;
name: string;
}>;

// Maps to react-router#https://reacttraining.com/react-router/web/api/Route
Expand Down
3 changes: 3 additions & 0 deletions frontend/packages/dev-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"private": true,
"dependencies": {
"@console/plugin-sdk": "0.0.0-fixed"
},
"consolePlugin": {
"entry": "src/plugin.tsx"
}
}
17 changes: 17 additions & 0 deletions frontend/packages/dev-console/src/components/AddPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import { Helmet } from 'react-helmet';
import ODCEmptyState from './EmptyState';
import NamespacedPage from './NamespacedPage';

const AddPage: React.FC = () => (
<React.Fragment>
<Helmet>
<title>+Add</title>
</Helmet>
<NamespacedPage>
<ODCEmptyState title="+Add" />
</NamespacedPage>
</React.Fragment>
);

export default AddPage;
18 changes: 18 additions & 0 deletions frontend/packages/dev-console/src/components/EmptyState.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.odc-empty-state {
// work around status-box injecting intermediate node preventing pages from controling the height of their content
&__title {
background-color: var(--pf-global--BackgroundColor--light-100);
display: flex;
flex-direction: column;
}

&__content {
padding: var(--pf-global--spacer--lg);
flex: 1;
}

&__card {
height: 100%;
text-align: center;
}
}
100 changes: 100 additions & 0 deletions frontend/packages/dev-console/src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as React from 'react';
import { Card, CardBody, CardHeader, CardFooter, Grid, GridItem } from '@patternfly/react-core';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { formatNamespacedRouteForResource } from '@console/internal/actions/ui';
import { PageHeading } from '@console/internal/components/utils';
import './EmptyState.scss';

interface StateProps {
activeNamespace: string;
}

export interface EmptySProps {
title: string;
}

type Props = EmptySProps & StateProps;

const ODCEmptyState: React.FunctionComponent<Props> = ({ title, activeNamespace }) => (
<React.Fragment>
<div className="odc-empty-state__title">
<PageHeading title={title} />
</div>
<div className="odc-empty-state__content">
<Grid gutter="md">
<GridItem sm={6} md={6} lg={4}>
<Card className="odc-empty-state__card">
<CardHeader>Import from Git</CardHeader>
<CardBody>Import code from your git repository to be built and deployed </CardBody>
<CardFooter>
<Link className="pf-c-button pf-m-secondary" to="/import">
Copy link
Member

Choose a reason for hiding this comment

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

We should consider adding RBAC checks. What should the user experience be if I don't have authority to add to the project?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I've raised your concern with UX. This is something we can add to our backlog for 4.2. We have a story to rework the empty state patterns with new UX. I've added your concerns there.

Import from Git
</Link>
</CardFooter>
</Card>
</GridItem>
<GridItem sm={6} md={6} lg={4}>
<Card className="odc-empty-state__card">
<CardHeader>Browse Catalog</CardHeader>
<CardBody>Browse the catalog to discover, deploy and connect to services</CardBody>
<CardFooter>
<Link className="pf-c-button pf-m-secondary" to="/catalog">
Browse Catalog
</Link>
</CardFooter>
</Card>
</GridItem>
<GridItem sm={6} md={6} lg={4}>
<Card className="odc-empty-state__card">
<CardHeader>Deploy Image</CardHeader>
<CardBody>Deploy an existing image from an image registry or image stream tag</CardBody>
<CardFooter>
<Link
className="pf-c-button pf-m-secondary"
to={`/deploy-image?preselected-ns=${activeNamespace}`}
>
Deploy Image
</Link>
</CardFooter>
</Card>
</GridItem>
<GridItem sm={6} md={6} lg={4}>
<Card className="odc-empty-state__card">
<CardHeader>Import YAML</CardHeader>
<CardBody>Create or replace resources from their YAML or JSON definitions.</CardBody>
<CardFooter>
<Link
className="pf-c-button pf-m-secondary"
to={formatNamespacedRouteForResource('import', activeNamespace)}
>
Import YAML
</Link>
</CardFooter>
</Card>
</GridItem>
<GridItem sm={6} md={6} lg={4}>
<Card className="odc-empty-state__card">
<CardHeader>Add Database</CardHeader>
<CardBody>
Browse the catalog to discover database services to add to your application
</CardBody>
<CardFooter>
<Link className="pf-c-button pf-m-secondary" to="/catalog?category=databases">
Add Database
</Link>
</CardFooter>
</Card>
</GridItem>
</Grid>
</div>
</React.Fragment>
);

const mapStateToProps = (state): StateProps => {
return {
activeNamespace: state.UI.get('activeNamespace'),
};
};

export default connect<StateProps>(mapStateToProps)(ODCEmptyState);
23 changes: 23 additions & 0 deletions frontend/packages/dev-console/src/components/NamespacedPage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.odc-namespaced-page {
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;

&__content {
flex-grow: 1;
position: relative;
overflow: auto;
background-color: var(--pf-global--Color--light-200);
}

// Override styles of namespace bar to support the addition of the application selector on mobile
& .co-namespace {
&-selector {
max-width: 100%;
}
&-bar__items {
flex-wrap: wrap;
}
}
}
16 changes: 16 additions & 0 deletions frontend/packages/dev-console/src/components/NamespacedPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import { NamespaceBar } from '@console/internal/components/namespace';
import ApplicationSelector from './dropdown/ApplicationSelector';

import './NamespacedPage.scss';

const NamespacedPage: React.FC = ({ children }) => (
<div className="odc-namespaced-page">
<NamespaceBar>
<ApplicationSelector />
</NamespaceBar>
<div className="odc-namespaced-page__content">{children}</div>
</div>
);

export default NamespacedPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import configureMockStore from 'redux-mock-store';
import { Map as ImmutableMap } from 'immutable';
import { shallow } from 'enzyme';
import ConnectedEmptyStateComponent from '../EmptyState';
import { getStoreTypedComponent } from '../../test/test-utils';

describe('EmptyState', () => {
const mockStore = configureMockStore();
const ConnectedComponent = getStoreTypedComponent(ConnectedEmptyStateComponent);

it('should pass activeNamespace from state as prop', () => {
const store = mockStore({
UI: ImmutableMap({
activeNamespace: 'project',
}),
});

const topologyWrapper = shallow(<ConnectedComponent store={store} title="" />);

expect(topologyWrapper.props().activeNamespace).toEqual('project');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import { FormGroup, ControlLabel, FormControl, HelpBlock } from 'patternfly-react';
import ApplicationDropdown from './ApplicationDropdown';

const CREATE_APPLICATION_KEY = 'create-application-key';

interface AppNameSelectorProps {
namespace?: string;
application: string;
selectedKey: string;
onChange?: (name: string, key: string) => void;
}

const AppNameSelector: React.FC<AppNameSelectorProps> = ({
application,
namespace,
selectedKey,
onChange,
}) => {
const onDropdownChange = (appName: string, key: string) => {
if (key === CREATE_APPLICATION_KEY) {
onChange('', key);
} else {
onChange(appName, key);
}
};

const onInputChange: React.ReactEventHandler<HTMLInputElement> = (event) => {
onChange(event.currentTarget.value, selectedKey);
};

return (
<React.Fragment>
<FormGroup>
<ControlLabel className="co-required">Application</ControlLabel>
<ApplicationDropdown
dropDownClassName="dropdown--full-width"
menuClassName="dropdown-menu--text-wrap"
namespace={namespace}
actionItem={{
actionTitle: 'Create New Application',
actionKey: CREATE_APPLICATION_KEY,
}}
selectedKey={selectedKey}
onChange={onDropdownChange}
/>
</FormGroup>
{selectedKey === CREATE_APPLICATION_KEY ? (
<FormGroup>
<ControlLabel className="co-required">Application Name</ControlLabel>
<FormControl
className="form-control"
type="text"
onChange={onInputChange}
value={application}
aria-describedby="name-help"
required
/>
<HelpBlock>Names the application.</HelpBlock>
</FormGroup>
) : null}
</React.Fragment>
);
};

export default AppNameSelector;
Loading