From 69f624dc51d35197a1f2c2624240914f5674cd5e Mon Sep 17 00:00:00 2001 From: Daniel Florian Date: Mon, 17 Jun 2019 06:58:02 -0500 Subject: [PATCH] Work on integration-related empty state components and fix NaN in activity log - added empty state to dashboard's TopIntegrationCard - added empty state to integrations list - fixed issues in metrics page when not all metrics are available - changed data helpers util to return '0 ms' when duration is zero. --- .../ui/src/Dashboard/TopIntegrationsCard.tsx | 21 ++++++- .../Integration/IntegrationsEmptyState.tsx | 50 ++++++++++++++++ .../src/Integration/IntegrationsListView.tsx | 16 ++++- .../Metrics/IntegrationDetailMetrics.css | 8 +++ .../Metrics/IntegrationDetailMetrics.tsx | 37 ++++++++---- .../packages/ui/src/Integration/index.ts | 1 + .../Integration/IntegrationDetail.stories.tsx | 24 ++++---- .../IntegrationsEmptyState.stories.tsx | 46 +++++++++++++++ .../IntegrationDetailsMetrics.stories.tsx | 47 +++++++++++++++ .../packages/utils/src/dateHelpers.ts | 5 +- .../modules/dashboard/pages/DashboardPage.tsx | 13 ++++ .../locales/integrations-translations.en.json | 54 +++++++++-------- .../locales/integrations-translations.it.json | 15 ++++- .../integrations/pages/IntegrationsPage.tsx | 11 +++- .../integrations/pages/detail/MetricsPage.tsx | 59 ++++++++++++------- 15 files changed, 334 insertions(+), 73 deletions(-) create mode 100644 app/ui-react/packages/ui/src/Integration/IntegrationsEmptyState.tsx create mode 100644 app/ui-react/packages/ui/stories/Integration/IntegrationsEmptyState.stories.tsx create mode 100644 app/ui-react/packages/ui/stories/Integration/Metrics/IntegrationDetailsMetrics.stories.tsx diff --git a/app/ui-react/packages/ui/src/Dashboard/TopIntegrationsCard.tsx b/app/ui-react/packages/ui/src/Dashboard/TopIntegrationsCard.tsx index 3d038e2f574..e24ac70c903 100644 --- a/app/ui-react/packages/ui/src/Dashboard/TopIntegrationsCard.tsx +++ b/app/ui-react/packages/ui/src/Dashboard/TopIntegrationsCard.tsx @@ -1,11 +1,18 @@ +import * as H from '@syndesis/history'; import { Card } from 'patternfly-react'; import * as React from 'react'; +import { IntegrationsEmptyState } from '../Integration'; import './TopIntegrations.css'; export interface ITopIntegrationsProps { + i18nCreateIntegration: string; + i18nCreateIntegrationTip?: string; + i18nEmptyStateInfo: string; + i18nEmptyStateTitle: string; i18nLast30Days: string; i18nTitle: string; + linkCreateIntegration: H.LocationDescriptor; } export class TopIntegrationsCard extends React.Component< @@ -20,7 +27,19 @@ export class TopIntegrationsCard extends React.Component< {this.props.i18nLast30Days} - {this.props.children} + + {this.props.children ? ( + this.props.children + ) : ( + + )} + ); } diff --git a/app/ui-react/packages/ui/src/Integration/IntegrationsEmptyState.tsx b/app/ui-react/packages/ui/src/Integration/IntegrationsEmptyState.tsx new file mode 100644 index 00000000000..335bd636602 --- /dev/null +++ b/app/ui-react/packages/ui/src/Integration/IntegrationsEmptyState.tsx @@ -0,0 +1,50 @@ +import * as H from '@syndesis/history'; +import { EmptyState, OverlayTrigger, Tooltip } from 'patternfly-react'; +import * as React from 'react'; +import { ButtonLink } from '../Layout'; + +export interface IIntegrationsEmptyStateProps { + i18nCreateIntegration: string; + i18nCreateIntegrationTip?: string; + i18nEmptyStateInfo: string; + i18nEmptyStateTitle: string; + linkCreateIntegration: H.LocationDescriptor; +} + +export class IntegrationsEmptyState extends React.Component< + IIntegrationsEmptyStateProps +> { + public getCreateIntegrationTooltip() { + return ( + + {this.props.i18nCreateIntegrationTip + ? this.props.i18nCreateIntegrationTip + : this.props.i18nCreateIntegration} + + ); + } + + public render() { + return ( + + + {this.props.i18nEmptyStateTitle} + {this.props.i18nEmptyStateInfo} + + + + {this.props.i18nCreateIntegration} + + + + + ); + } +} diff --git a/app/ui-react/packages/ui/src/Integration/IntegrationsListView.tsx b/app/ui-react/packages/ui/src/Integration/IntegrationsListView.tsx index 9e66f71a6ff..cda666c5dd6 100644 --- a/app/ui-react/packages/ui/src/Integration/IntegrationsListView.tsx +++ b/app/ui-react/packages/ui/src/Integration/IntegrationsListView.tsx @@ -6,6 +6,7 @@ import { ListViewToolbar, SimplePageHeader, } from '../Shared'; +import { IntegrationsEmptyState } from './IntegrationsEmptyState'; export interface IIntegrationsListViewProps extends IListViewToolbarProps { linkToManageCiCd: H.LocationDescriptor; @@ -16,6 +17,9 @@ export interface IIntegrationsListViewProps extends IListViewToolbarProps { i18nManageCiCd: string; i18nImport: string; i18nLinkCreateConnection: string; + i18nLinkCreateIntegrationTip?: string; + i18nEmptyStateInfo: string; + i18nEmptyStateTitle: string; } export class IntegrationsListView extends React.Component< @@ -52,7 +56,17 @@ export class IntegrationsListView extends React.Component< - {this.props.children} + {this.props.children ? ( + this.props.children + ) : ( + + )} ); diff --git a/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.css b/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.css index bdd136ca6b2..11c11ad083f 100644 --- a/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.css +++ b/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.css @@ -3,6 +3,14 @@ padding-bottom: 10px; } +.integration-detail-metrics__duration-difference { + font-size: smaller; +} + +.integration-detail-metrics__last-processed { + font-size: smaller; +} + .integration-detail-metrics__uptime-header { display: flex; justify-content: space-between; diff --git a/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.tsx b/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.tsx index 19911168957..6c6df2dde71 100644 --- a/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.tsx +++ b/app/ui-react/packages/ui/src/Integration/Metrics/IntegrationDetailMetrics.tsx @@ -17,6 +17,7 @@ import './IntegrationDetailMetrics.css'; export interface IIntegrationDetailMetricsProps { i18nLastProcessed: string; + i18nNoDataAvailable: string; i18nSince: string; i18nTotalErrors: string; i18nTotalMessages: string; @@ -34,7 +35,7 @@ export class IntegrationDetailMetrics extends React.Component< public render() { const okMessagesCount = this.props.messages! - this.props.errors!; const startAsDate = new Date(this.props.start!); - const startAsHuman = startAsDate.toLocaleString(); + const startAsHuman = this.props.i18nSince + startAsDate.toLocaleString(); return ( @@ -52,7 +53,7 @@ export class IntegrationDetailMetrics extends React.Component< - {this.props.errors} + {this.props.errors ? this.props.errors : 0} @@ -68,8 +69,10 @@ export class IntegrationDetailMetrics extends React.Component< {this.props.i18nLastProcessed} - - {this.props.lastProcessed} + + {this.props.lastProcessed + ? this.props.lastProcessed + : this.props.i18nNoDataAvailable} @@ -84,7 +87,7 @@ export class IntegrationDetailMetrics extends React.Component< > - {this.props.messages}  + {this.props.messages ? this.props.messages : 0}  {this.props.i18nTotalMessages} @@ -92,11 +95,15 @@ export class IntegrationDetailMetrics extends React.Component< - {okMessagesCount}  + {this.props.errors !== undefined && + this.props.messages !== undefined + ? okMessagesCount + : 0} +   - {this.props.errors} + {this.props.errors ? this.props.errors : 0} @@ -111,15 +118,19 @@ export class IntegrationDetailMetrics extends React.Component< >
{this.props.i18nUptime}
-
- {this.props.i18nSince} - {startAsHuman} -
+ {this.props.start !== undefined && + this.props.durationDifference !== undefined && ( +
+ {startAsHuman} +
+ )}
- - {this.props.durationDifference} + + {this.props.durationDifference !== undefined + ? this.props.durationDifference + : this.props.i18nNoDataAvailable} diff --git a/app/ui-react/packages/ui/src/Integration/index.ts b/app/ui-react/packages/ui/src/Integration/index.ts index dc0be645093..b2f9f8f8621 100644 --- a/app/ui-react/packages/ui/src/Integration/index.ts +++ b/app/ui-react/packages/ui/src/Integration/index.ts @@ -16,6 +16,7 @@ export * from './IntegrationFlowStepGeneric'; export * from './IntegrationFlowStepWithOverview'; export * from './IntegrationIcon'; export * from './IntegrationSaveForm'; +export * from './IntegrationsEmptyState'; export * from './IntegrationStatus'; export * from './IntegrationStatusDetail'; export * from './IntegrationStepsHorizontalItem'; diff --git a/app/ui-react/packages/ui/stories/Integration/IntegrationDetail.stories.tsx b/app/ui-react/packages/ui/stories/Integration/IntegrationDetail.stories.tsx index 7cffe78c0b1..a9278811ef6 100644 --- a/app/ui-react/packages/ui/stories/Integration/IntegrationDetail.stories.tsx +++ b/app/ui-react/packages/ui/stories/Integration/IntegrationDetail.stories.tsx @@ -223,18 +223,20 @@ storiesOf('Integration/Detail', module) + - )); diff --git a/app/ui-react/packages/ui/stories/Integration/IntegrationsEmptyState.stories.tsx b/app/ui-react/packages/ui/stories/Integration/IntegrationsEmptyState.stories.tsx new file mode 100644 index 00000000000..75fe24a94ad --- /dev/null +++ b/app/ui-react/packages/ui/stories/Integration/IntegrationsEmptyState.stories.tsx @@ -0,0 +1,46 @@ +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { IntegrationsEmptyState } from '../../src'; + +const stories = storiesOf('Integration/IntegrationsEmptyState', module); + +const createTip = 'Click to begin creating a new integration'; +const info = + 'There are currently no integrations. Click the button below to create one.'; +const link = '/integrations/create/new-integration/start/abcdefg'; +const title = 'Create Integration'; + +const storyNotes = + '- Verify title is "' + + title + + '"\n' + + '- Verify info is "' + + info + + '"\n' + + '- Verify button text is "' + + title + + '"\n' + + '- Verify button toolipt is "' + + createTip + + '"\n' + + '- Verify clicking button prints "' + + link + + '" in the **Actions** tab'; + +stories.add( + 'render', + () => ( + + + + ), + { notes: storyNotes } +); diff --git a/app/ui-react/packages/ui/stories/Integration/Metrics/IntegrationDetailsMetrics.stories.tsx b/app/ui-react/packages/ui/stories/Integration/Metrics/IntegrationDetailsMetrics.stories.tsx new file mode 100644 index 00000000000..5548181a9aa --- /dev/null +++ b/app/ui-react/packages/ui/stories/Integration/Metrics/IntegrationDetailsMetrics.stories.tsx @@ -0,0 +1,47 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { + IIntegrationDetailMetricsProps, + IntegrationDetailMetrics, +} from '../../../src'; + +const stories = storiesOf( + 'Integration/Metrics/IntegrationDetailMetrics', + module +); + +const durationDifference = '3:15:59'; +const errors = 2; +const i18nLastProcessed = 'Last Processed'; +const i18nNoDataAvailable = 'No data available'; +const i18nSince = 'Since '; +const i18nTotalErrors = 'Total Errors'; +const i18nTotalMessages = 'Total Messages'; +const i18nUptime = 'Uptime'; +const lastProcessed = '2 May 2019 08:19:42 GMT'; +const messages = 26126; +const start = 2323342333; + +const propsOnlyRequired = { + i18nLastProcessed, + i18nNoDataAvailable, + i18nSince, + i18nTotalErrors, + i18nTotalMessages, + i18nUptime, +} as IIntegrationDetailMetricsProps; + +const propsAll = { + ...propsOnlyRequired, + durationDifference, + errors, + lastProcessed, + messages, + start, +} as IIntegrationDetailMetricsProps; + +stories + + .add('empty state', () => ) + + .add('all props', () => ); diff --git a/app/ui-react/packages/utils/src/dateHelpers.ts b/app/ui-react/packages/utils/src/dateHelpers.ts index 526b742bc65..381017db2cc 100644 --- a/app/ui-react/packages/utils/src/dateHelpers.ts +++ b/app/ui-react/packages/utils/src/dateHelpers.ts @@ -31,9 +31,12 @@ export function toDurationString( timeDuration: number, unit: 'ms' | 'ns' ): string { - if (!timeDuration) { + if (timeDuration === undefined) { return 'NaN'; } + if (timeDuration === 0) { + return '0 ms'; + } if (unit === 'ns') { timeDuration = timeDuration / 1000000; } diff --git a/app/ui-react/syndesis/src/modules/dashboard/pages/DashboardPage.tsx b/app/ui-react/syndesis/src/modules/dashboard/pages/DashboardPage.tsx index de6e453e988..aecc5c61a82 100644 --- a/app/ui-react/syndesis/src/modules/dashboard/pages/DashboardPage.tsx +++ b/app/ui-react/syndesis/src/modules/dashboard/pages/DashboardPage.tsx @@ -184,12 +184,25 @@ export default () => ( } topIntegrations={ click to select files using a file chooser dialog.", "ImportIntegrationInvalidFileMessage": "", "ImportIntegrationFailedMessage": "", "ImportIntegration": "Import Integration", "ImportIntegrationDescription": "Choose one or more zip files that contain exported integrations that you want to import.", - "ImportHelpMessage": "Note: The imported integration will be in the draft state. If you previously imported and this environment has a draft version of the integration, then that draft is lost.", + "ImportHelpMessage": "Note: The imported integration will be in the draft state. If you previously imported and this environment has a draft version of the integration, then that draft is lost.", "ImportSelectedFileLabel": "Selected file:", "ImportNoFileSelectedMessage": "no file selected", - "ImportUploadFailedAlertMessage": "File '{{fileName}}' could not be uploaded.", - "ImportUploadFailedMessage": "Upload failed for {{fileName}}.", - "ImportUploadSuccessMessage": "Successfully imported {{fileName}}.", - "PublishingIntegrationMessage": "Starting integration. Your integration will start running in a few moments.", - "UnpublishingIntegrationMessage": "Stopping integration. It takes a few moments to stop the integration.", - "DeletingIntegrationMessage": "Deleting integration. It takes a few moments to delete the integration.", + "ImportUploadFailedAlertMessage": "File '{{fileName}}' could not be uploaded", + "ImportUploadFailedMessage": "Upload failed for {{fileName}}", + "ImportUploadSuccessMessage": "Successfully imported {{fileName}}", + "PublishingIntegrationMessage": "Starting integration. Your integration will start running in a few moments.", + "UnpublishingIntegrationMessage": "Stopping integration. It takes a few moments to stop the integration.", + "DeletingIntegrationMessage": "Deleting integration. It takes a few moments to delete the integration.", "PublishingIntegrationFailedMessage": "Error starting integration: {{error}}", "UnpublishingIntegrationFailedMessage": "Error stopping integration: {{error}}", "DeletingIntegrationFailedMessage": "Error deleting integration: {{error}}", @@ -59,18 +59,19 @@ }, "metrics": { "lastProcessed": "Last Processed", + "NoDataAvailable": "No data available", "since": "Since ", "totalErrors": "Total Errors", "totalMessages": "Total Messages", "uptime": "Uptime" }, "alerts": { - "modified": "A connection associated with this integration has been modified. To incorporate the changes, edit the integration.", - "obsolete": "The published integration has become obsolete. The recommendation is to republish it." + "modified": "A connection associated with this integration has been modified. To incorporate the changes, please edit the integration.", + "obsolete": "The published integration has become obsolete and its recommended it should be republished." }, "editor": { "addStep": "Add a step", - "addStepDescription": "You can continue adding steps and connections to your integration.", + "addStepDescription": "You can continue adding steps and connections to your integration as well.", "addToIntegration": "Add to Integration", "confirmDeleteStepDialogBody": "Are you sure you want to delete this step from the integration?", "confirmDeleteStepDialogTitle": "Confirm Delete", @@ -81,12 +82,12 @@ "saveAndPublish": "Save and publish" }, "choiceForm": { - "conditionDescription": "Provide a condition that you want to evaluate (for example, ${in.header.type} == 'note' or ${in.body.title} contains 'Important').", + "conditionDescription": "Provide a condition that you want to evaluate (e.g. ${in.header.type} == 'note' or ${in.body.title} contains 'Important').", "conditionName": "Condition", "conditionPlaceholder": "Simple language expression", "addCondition": "+ Add another condition", "addConditionTitle": "When", - "useDefaultFlowDescription": "Execute this flow when no other condition matches.", + "useDefaultFlowDescription": "Use this flow when no other condition matches", "useDefaultFlowTitle": "Use a default flow", "fieldRequired": "{{field}} is required" }, @@ -113,7 +114,7 @@ }, "reviewActions": { "btnReviewEdit": "Review/Edit", - "description": "To examine and optionally update the operations defined in the OpenAPI document, click Review/Edit below.", + "description": "This description has not yet been actually defined, please send help.", "descriptionLabel": "Description", "nameLabel": "Name", "operations": "operations", @@ -125,7 +126,7 @@ }, "reviewOperations": { "title": "Operations", - "description": "An API provider integration has a flow for each operation. Select an operation to add to its flow." + "description": "An integration can have multiple flows. Select an operation to start creating a flow." }, "selectMethod": { "description": "Execute this integration when a client invokes an operation defined by this API.", @@ -133,14 +134,14 @@ "dndUploadSuccessMessage": "Successfully uploaded ", "dndFileExtensions": ".json,.yaml,.yml", "dndHelpMessage": "Accepted file type: .json, .yaml, and .yml", - "dndInstructions": "Drag and drop a file here, or click to select a file by using a file chooser dialog.", + "dndInstructions": "Drag 'n' drop a file here, or click to select a file using a file chooser dialog.", "dndNoFileSelectedLabel": "No file selected", "dndSelectedFileLabel": "Selected file:", "methodFromFile": "Upload an OpenAPI file", "methodFromScratch": "Create from scratch", "methodFromUrl": "Use a URL", "title": "Start integration with an API call", - "urlNote": "* Note: After uploading this document, updates to it are not automatically obtained." + "urlNote": "* Note: After uploading this document, Syndesis does not automatically obtain any updates to it." }, "setInfo": { "title": "Give this integration a name" @@ -150,7 +151,7 @@ "ReplaceDraftModalMessage": "Are you sure you want to to replace the current draft for the \"{{name}}\" integration?", "ReplaceDraftMOdalTitle": "Replace Draft?", "ReplacedDraftMessage": "Updating draft. Replacing the current draft of the integration. ", - "publishDeploymentModal": "Are you sure you want to start version \"{{version}}\" of integration \"{{name}}\"?", + "publishDeploymentModal": "Are you sure you want to start version \"{{version}}\" of integration \"{{name}}\"", "publishDeploymentModalTitle": "Start Deployment?", "ReplaceDraftFailedMessage": "Failed to replace the current draft: {{error}}", "NoErrors": "No errors", @@ -166,7 +167,7 @@ "templater-validation-errors": "Vaidation errors", "templater-file-upload-dnd": "Drag and drop your template file here", "templater-upload-invalid-file": "'{{0}}' is not a valid file. Only text files can be uploaded.", - "templater-url-upload": "Use a URL", + "templater-url-upload": "Use an URL", "templater-url-upload-note": "* Note: After uploading this template, {{shared.project.name}} does not automatically obtain any updates to it. You would have to edit the step and re-upload the template to incorporates the updates.", "templater-create": "Create", "templater-create-editor-title": "Copy and paste a template or enter text that defines a template.", @@ -191,5 +192,10 @@ "linter-expected-close-symbol": "Expected close symbol at line: {{0}}, column: {{1}}", "linter-no-symbols": "The editor contains no data-mappable symbols", "linter-no-content": "The editor has no content" + }, + "integrationsEmptyState": { + "createTip": "Click to being creating an integration", + "info": "There are currently no integrations. Click the button below to create one.", + "title": "$t(shared:linkCreateIntegration)" } } diff --git a/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.it.json b/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.it.json index 9e726e7aa1b..edbeaa626da 100644 --- a/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.it.json +++ b/app/ui-react/syndesis/src/modules/integrations/locales/integrations-translations.it.json @@ -6,5 +6,18 @@ "ASSEMBLING": "Assemblaggio", "BUILDING": "Costruzione", "DEPLOYING": "La Diffusione", - "STARTING": "Di Partenza" + "STARTING": "Di Partenza", + "metrics": { + "lastProcessed": "Ultima Elaborazione", + "NoDataAvailable": "Nessun dato disponibile", + "since": "Da ", + "totalErrors": "Errori Totali", + "totalMessages": "Messaggi Totali", + "uptime": "Tempo" + }, + "integrationsEmptyState": { + "createTip": "Fare clic per iniziare a creare un'integrazione", + "info": "Al momento non ci sono integrazioni. Fai clic sul pulsante qui sotto per crearne uno.", + "title": "$t(shared:linkCreateIntegration)" + } } diff --git a/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx b/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx index 1b7963d207a..57d5ce93a5c 100644 --- a/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/pages/IntegrationsPage.tsx @@ -143,13 +143,22 @@ export class IntegrationsPage extends React.Component { i18nTitle={t('shared:Integrations')} i18nDescription={t('integrationListDescription')} i18nImport={t('shared:Import')} - i18nManageCiCd={t('integrations:ManageCiCd')} + i18nManageCiCd={t('ManageCiCd')} i18nLinkCreateConnection={t( 'shared:linkCreateIntegration' )} + i18nLinkCreateIntegrationTip={t( + 'integrationsEmptyState.createTip' + )} i18nResultsCount={t('shared:resultsCount', { count: filteredAndSortedIntegrations.length, })} + i18nEmptyStateInfo={t( + 'integrationsEmptyState.info' + )} + i18nEmptyStateTitle={t( + 'integrationsEmptyState.title' + )} > );