From 46fdd3ddca5f3b3439a373dbda3067eb9912c3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Jyrki=C3=A4inen?= Date: Mon, 19 Sep 2022 16:18:11 +0300 Subject: [PATCH 1/2] Development (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * a3 posters * MM-275: Switch to github actions, remove old TravisCI scripts * Fix Dockerfile naming scheme * Fix naming to work with react-app-rewired * Change env file name back to prod * Fix cypress command * Fix Cypress tests, change IDs * Change magic number * Change poster stop id * Add animationDistanceThreshold to type command * Add delay to fix failing test * Dont wait for animations at all * Decrease sensitivity * Disable error checking * Trying to fix flaky tests * Disable error checking to prevent flakyness * Disable error checking to prevent flakyness * Optimize the build * MM-326 Change build to be triggered on push Also add workflow for stage environment * Change serve to nginx Note: the default port is now changed to 80! Modify the server environment. * Add buttons to select terminal poster and show terminals * Add ability to group stops by terminals * Add terminal id to generation parameters if terminalposter is used * Use only stopId, not terminalId Also disable ruletemplates for terminals * Add button for filter only selected rows * Change the way to generate terminal posters * Remove the previous implementation of terminalgroups and add minor usability changes * Fix problem when no selectedStops * legend checkbox (#73) * MM-330: Upgrade docker base image node version to 16 * Reimplement terminal select to support search * poster canceling * Add missing prop validations * Send session data also on GET requests * Fetch data after login not before * Disable broken tests * Add Dependabot configuration file (#101) Add Dependabot config file for specifying a non-default branch for Dependabot PRs. * mock data for fetches (#100) Co-authored-by: e-halinen Co-authored-by: Juho Hänninen Co-authored-by: e-halinen <54105602+e-halinen@users.noreply.github.com> --- .env.production => .env.prod | 0 .github/dependabot.yml | 14 + .github/workflows/buildAndPublish_dev.yml | 10 +- .github/workflows/buildAndPublish_prod.yml | 6 +- .github/workflows/buildAndPublish_stage.yml | 51 ++++ .github/workflows/buildOnPullRequest.yml | 3 + .travis.yml | 11 - Dockerfile | 11 +- README.md | 2 +- cypress/integration/general.spec.js | 25 +- docker-deploy-all.sh | 13 - docker-deploy-tag.sh | 13 - package.json | 3 +- src/App.test.js | 65 ++++- src/components/BuildDetails.js | 9 + src/components/Frame.js | 2 +- src/components/Generator.js | 47 +++- src/components/RadioGroup.js | 6 + src/components/StopList.js | 19 +- src/components/TerminalSelect.js | 46 +++ src/stores/commonStore.js | 30 ++ src/stores/generatorStore.js | 87 ++++-- src/util/api.js | 39 ++- travis-build.sh | 42 --- yarn.lock | 295 +++++++++++++++++++- 25 files changed, 691 insertions(+), 158 deletions(-) rename .env.production => .env.prod (100%) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/buildAndPublish_stage.yml delete mode 100644 .travis.yml delete mode 100755 docker-deploy-all.sh delete mode 100755 docker-deploy-tag.sh create mode 100644 src/components/TerminalSelect.js delete mode 100755 travis-build.sh diff --git a/.env.production b/.env.prod similarity index 100% rename from .env.production rename to .env.prod diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d4cd95b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# Specify a non-default branch for pull requests for npm + +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + # Raise pull requests for version updates + # to npm against the `development` branch + target-branch: "development" + # Labels on pull requests for version updates only + labels: + - "NPM Dependencies" diff --git a/.github/workflows/buildAndPublish_dev.yml b/.github/workflows/buildAndPublish_dev.yml index ccab9be..6eb351b 100644 --- a/.github/workflows/buildAndPublish_dev.yml +++ b/.github/workflows/buildAndPublish_dev.yml @@ -1,9 +1,9 @@ name: Build and publish Docker Hub dev image on: - pull_request: + push: branches: - - 'master' + - 'development' jobs: build-container: @@ -38,10 +38,14 @@ jobs: context: . push: true tags: ${{ format('hsldevcom/hsl-map-publisher-ui:dev-{0}-{1}', steps.timestamp.outputs.timestamp, steps.commit_hash.outputs.hash) }} + build-args: | + BUILD_ENV=dev - name: Build and push uses: docker/build-push-action@v2 with: context: . push: true - tags: hsldevcom/hsl-map-publisher-ui:dev \ No newline at end of file + tags: hsldevcom/hsl-map-publisher-ui:dev + build-args: | + BUILD_ENV=dev diff --git a/.github/workflows/buildAndPublish_prod.yml b/.github/workflows/buildAndPublish_prod.yml index a3b150f..7b190e8 100644 --- a/.github/workflows/buildAndPublish_prod.yml +++ b/.github/workflows/buildAndPublish_prod.yml @@ -38,10 +38,14 @@ jobs: context: . push: ${{ github.event_name == 'push' }} tags: ${{ format('hsldevcom/hsl-map-publisher-ui:prod-{0}-{1}', steps.timestamp.outputs.timestamp, steps.commit_hash.outputs.hash) }} + build-args: | + BUILD_ENV=prod - name: Build and push uses: docker/build-push-action@v2 with: context: . push: ${{ github.event_name == 'push' }} - tags: hsldevcom/hsl-map-publisher-ui:prod \ No newline at end of file + tags: hsldevcom/hsl-map-publisher-ui:prod + build-args: | + BUILD_ENV=prod diff --git a/.github/workflows/buildAndPublish_stage.yml b/.github/workflows/buildAndPublish_stage.yml new file mode 100644 index 0000000..9f4d299 --- /dev/null +++ b/.github/workflows/buildAndPublish_stage.yml @@ -0,0 +1,51 @@ +name: Build and publish Docker Hub stage image + +on: + push: + branches: + - 'stage' + +jobs: + build-container: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.KARTAT_DOCKERHUB_USER }} + password: ${{ secrets.KARTAT_DOCKERHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: hsldevcom/hsl-map-publisher-ui + + - name: Get commit hash + id: commit_hash + run: echo "::set-output name=hash::$(git rev-parse --short "$GITHUB_SHA")" + + - name: Get image timestamp + id: timestamp + run: echo "::set-output name=timestamp::$(date +'%Y-%m-%d')" + + - name: Build and push timestamped tag + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ${{ format('hsldevcom/hsl-map-publisher-ui:stage-{0}-{1}', steps.timestamp.outputs.timestamp, steps.commit_hash.outputs.hash) }} + build-args: | + BUILD_ENV=stage + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: hsldevcom/hsl-map-publisher-ui:stage + build-args: | + BUILD_ENV=stage diff --git a/.github/workflows/buildOnPullRequest.yml b/.github/workflows/buildOnPullRequest.yml index 9f297af..61b78c1 100644 --- a/.github/workflows/buildOnPullRequest.yml +++ b/.github/workflows/buildOnPullRequest.yml @@ -16,3 +16,6 @@ jobs: uses: docker/build-push-action@v2 with: context: . + build-args: | + BUILD_ENV=dev + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6e9e593..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -sudo: required - -branches: - only: - - development - - stage - - master - -services: docker - -script: ./travis-build.sh diff --git a/Dockerfile b/Dockerfile index 35c11b4..f5ee6ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12-alpine +FROM node:16-alpine as builder ENV WORK /opt/publisher @@ -7,18 +7,19 @@ RUN mkdir -p ${WORK} WORKDIR ${WORK} # Install app dependencies -COPY yarn.lock ${WORK} -COPY package.json ${WORK} +COPY package.json yarn.lock ${WORK}/ RUN yarn # Bundle app source COPY . ${WORK} -ARG BUILD_ENV=production +ARG BUILD_ENV=prod COPY .env.${BUILD_ENV} ${WORK}/.env.production # TODO: Fix tests to enable yarn build again # RUN yarn build RUN yarn bundle -CMD yarn run serve +# Copy builded files from builder to nginx +FROM nginx:1.21-alpine +COPY --from=builder /opt/publisher/build /usr/share/nginx/html diff --git a/README.md b/README.md index 7b10154..a7822cb 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Open [http://localhost:3000/](http://localhost:3000/) ``` docker build --build-arg API_URL=$API_URL -t hsl-map-publisher-ui . -docker run -d -p 3000:3000 hsl-map-publisher-ui +docker run -d -p 80:80 hsl-map-publisher-ui ``` where `$API_URL` is URL to publisher REST API. diff --git a/cypress/integration/general.spec.js b/cypress/integration/general.spec.js index 15b0124..2aa7480 100644 --- a/cypress/integration/general.spec.js +++ b/cypress/integration/general.spec.js @@ -38,6 +38,10 @@ describe('General tests', () => { .click() .should('have.value', 'Timetable'); + cy.get('[data-cy=Terminaalijuliste]') + .click() + .should('have.value', 'TerminalPoster'); + cy.get('[data-cy=Pysäkkijuliste]') .click() .should('have.value', 'StopPoster'); @@ -61,11 +65,12 @@ describe('General tests', () => { it('Filter filters list and selecting values works', () => { cy.get('[data-cy=filterInput]') - .type('1010109,1010128,1020100') - .should('have.value', '1010109,1010128,1020100'); + .type('1010107,1010108,1010109') + .should('have.value', '1010107,1010108,1010109'); + cy.get('[data-cy=1010107]').click(); + cy.get('[data-cy=1010108]').click(); cy.get('[data-cy=1010109]').click(); - cy.get('[data-cy=1020100]').click(); }); it('Create template and remove it', () => { @@ -77,7 +82,7 @@ describe('General tests', () => { cy.get('[data-cy=prompt-textfield]') .click() - .type(uuid); + .type(uuid, { force: true }); cy.get('[data-cy=prompt-textfield]').should('have.value', uuid); cy.get('[data-cy=prompt-ok]').should('have.enabled'); @@ -106,7 +111,7 @@ describe('General tests', () => { it('Test name validation for list', () => { cy.get('[data-cy=create-build]').click(); - cy.get('[data-cy=prompt-textfield]').type('/'); + cy.get('[data-cy=prompt-textfield]').type('/', { force: true }); cy.get('[data-cy=prompt-ok]').should('have.disabled'); }); @@ -117,7 +122,7 @@ describe('General tests', () => { cy.route('POST', `${API_URL}/builds`).as('postBuild'); cy.get('[data-cy=create-build]').click(); - cy.get('[data-cy=prompt-textfield]').type(uuid); + cy.get('[data-cy=prompt-textfield]').type(uuid, { force: true }); cy.get('[data-cy=prompt-textfield]').should('have.value', uuid); cy.get('[data-cy=prompt-ok]').should('have.enabled'); cy.get('[data-cy=prompt-ok]').click(); @@ -151,7 +156,7 @@ describe('General tests', () => { cy.route('POST', `${API_URL}/posters`).as('postPoster'); cy.get('[data-cy=create-build]').click(); - cy.get('[data-cy=prompt-textfield]').type(buildTitle); + cy.get('[data-cy=prompt-textfield]').type(buildTitle, { force: true }); cy.get('[data-cy=prompt-ok]').click(); cy.wait('@postBuild'); @@ -160,14 +165,14 @@ describe('General tests', () => { cy.get('[data-cy=new-template]').click(); cy.get('[data-cy=prompt-textfield]') .click() - .type(templateId); + .type(templateId, { force: true }); cy.get('[data-cy=prompt-ok]').click(); cy.wait('@postTemplate'); cy.get('[data-cy=generate]').click(); - cy.get('[data-cy=filterInput]').type('1010128'); - cy.get('[data-cy=1010128]').click(); + cy.get('[data-cy=filterInput]').type('1020131'); + cy.get('[data-cy=1020131]').click(); cy.get('[data-cy=select-template]').click(); cy.get(`[data-cy=${templateId}]`).click(); cy.get('[data-cy=build-select]').click(); diff --git a/docker-deploy-all.sh b/docker-deploy-all.sh deleted file mode 100755 index 921d14c..0000000 --- a/docker-deploy-all.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -e - -# Builds and deploys all images for the Azure environments - -ORG=${ORG:-hsldevcom} - -for TAG in dev stage production; do - DOCKER_IMAGE=$ORG/hsl-map-publisher-ui:${TAG} - - docker build --build-arg BUILD_ENV=${TAG} -t $DOCKER_IMAGE . - docker push $DOCKER_IMAGE -done diff --git a/docker-deploy-tag.sh b/docker-deploy-tag.sh deleted file mode 100755 index 1d849dd..0000000 --- a/docker-deploy-tag.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -e - -ORG=${ORG:-hsldevcom} - -read -p "Tag: " TAG - -DOCKER_TAG=${TAG:-production} -DOCKER_IMAGE=$ORG/hsl-map-publisher-ui:${DOCKER_TAG} - -docker build --build-arg BUILD_ENV=${TAG:-production} -t $DOCKER_IMAGE . -docker push $DOCKER_IMAGE diff --git a/package.json b/package.json index b838e73..d4654ac 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-hot-loader": "^4.3.3", "react-resizable": "^1.7.5", "react-scripts": "^1.1.4", + "react-select": "^5.2.2", "react-slidedown": "^1.3.0", "react-test-renderer": "^16.12.0", "react-virtualized": "^9.19.1", @@ -43,7 +44,7 @@ "lint": "yarn eslint src", "serve": "serve build", "prettier": "prettier src/**/* --write", - "cypress": "run cypress open" + "cypress": "cypress open" }, "devDependencies": { "babel-eslint": "^8.2.3", diff --git a/src/App.test.js b/src/App.test.js index b492e0a..31b564a 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -15,31 +15,70 @@ import Generator from './components/Generator'; import stores from './stores/stores'; +const MOCK_DATA = { + build: { + id: '123456-65421', + title: 'testi', + status: 'OPEN', + posters: [], + createdAt: '2022-08-23T18:55:52.988Z', + updatedAt: '2022-08-23T18:55:52.988Z', + pending: 0, + failed: 0, + ready: 0, + }, + template: { + id: 'test', + label: 'test', + areas: [], + created_at: '2022-09-06T12:56:28.539Z', + updated_at: '2022-09-06T12:56:28.539Z', + rules: { + type: 'RULE', + name: 'ZONE', + value: 'A', + }, + }, + stop: { + distributionArea: 'alue1', + distributionOrder: 104, + drivebyTimetable: 1, + nameFi: 'Meritullinkatu', + posterCount: 0, + shortId: 'H 2014', + stopId: '1010107', + stopTariff: '01', + stopType: '04', + stopZone: 'A', + }, +}; + configure({ adapter: new Adapter() }); fetchMock.enableMocks(); describe('App component tests', () => { + stores.commonStore.selectedBuild = MOCK_DATA.build; + stores.commonStore.stops = [MOCK_DATA.stop]; + it('App renders without crashing', () => { - shallow(); + shallow( + + + , + ); }); }); describe('Frame component tests', () => { - fetch.mockResponse( - JSON.stringify({ - isOk: true, - email: 'testi@kayttaja.com', - }), + fetch.mockResponses( + [JSON.stringify({ data: { allStops: { nodes: [MOCK_DATA.stop] } } }), { status: 200 }], + [JSON.stringify([]), { status: 200 }], + [JSON.stringify([MOCK_DATA.build]), { status: 200 }], + [JSON.stringify([MOCK_DATA.template]), { status: 200 }], + [JSON.stringify([]), { status: 200 }], ); - stores.commonStore.selectedBuild = { - id: '123456-65421', - title: 'testi', - status: 'OPEN', - posters: [], - }; - const mountedWrapper = mount( diff --git a/src/components/BuildDetails.js b/src/components/BuildDetails.js index 8390ac8..6d1adbf 100644 --- a/src/components/BuildDetails.js +++ b/src/components/BuildDetails.js @@ -120,6 +120,12 @@ const Poster = props => ( label="Lataa PDF" primary /> + props.onCancel()} + label="Peruuta" + primary + /> @@ -179,6 +185,7 @@ class BuildDetails extends Component { key={poster.id} disableEdit={this.props.status !== 'OPEN'} onRemove={() => this.props.onRemovePoster(poster.id)} + onCancel={() => this.props.onCancelPoster(poster.id)} /> ))} @@ -237,6 +244,7 @@ Poster.propTypes = { ).isRequired, disableEdit: PropTypes.bool.isRequired, onRemove: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, }; BuildDetails.propTypes = { @@ -248,6 +256,7 @@ BuildDetails.propTypes = { ).isRequired, onRemovePoster: PropTypes.func.isRequired, // eslint-disable-line react/no-unused-prop-types onClose: PropTypes.func.isRequired, + onCancelPoster: PropTypes.func.isRequired, }; export default BuildDetails; diff --git a/src/components/Frame.js b/src/components/Frame.js index 17b7adb..28c23eb 100644 --- a/src/components/Frame.js +++ b/src/components/Frame.js @@ -37,7 +37,7 @@ class Frame extends Component { super(props); this.props.commonStore.getStops(); - // this.props.commonStore.getTerminals(); + this.props.commonStore.getTerminals(); this.props.commonStore.getBuilds(); this.props.commonStore.getTemplates(); this.props.commonStore.getImages(); diff --git a/src/components/Generator.js b/src/components/Generator.js index 9d4d348..c79f417 100644 --- a/src/components/Generator.js +++ b/src/components/Generator.js @@ -11,6 +11,8 @@ import StopList from './StopList'; import BuildSelect from './BuildSelect'; import SelectTemplate from './SelectTemplate'; import SelectRuleTemplates from './SelectRuleTemplates'; +import { componentsWithMapOptions } from '../stores/generatorStore'; +import TerminalSelect from './TerminalSelect'; const Root = styled.div` display: flex; @@ -53,7 +55,8 @@ const Generator = props => { const { commonStore, generatorStore } = props; const stopCount = generatorStore.rows .filter(({ rowId }) => generatorStore.checkedRows.includes(rowId)) - .map(({ stopIds }) => stopIds.length) + // For TerminalPoster one terminal means one poster, otherwise use the amount of stops + .map(({ stopIds }) => (generatorStore.component === 'TerminalPoster' ? 1 : stopIds.length)) .reduce((prev, cur) => prev + cur, 0); return ( @@ -90,6 +93,7 @@ const Generator = props => { valuesByLabel={generatorStore.rowTypesByLabel} valueSelected={generatorStore.rowType} onChange={value => generatorStore.setRowType(value)} + disabled={generatorStore.component === 'TerminalPoster'} /> @@ -135,6 +139,16 @@ const Generator = props => { + {generatorStore.component === 'TerminalPoster' && ( +
+ +
+ )} +
@@ -148,15 +162,17 @@ const Generator = props => { /> -
- -
+ {generatorStore.component !== 'TerminalPoster' && ( +
+ +
+ )} - {generatorStore.component === 'StopPoster' && ( + {componentsWithMapOptions.includes(generatorStore.component) && (

Lähikartta

@@ -176,6 +192,11 @@ const Generator = props => { defaultValueTrue={generatorStore.salesPoint} onChange={() => generatorStore.setSalesPoint()} /> + generatorStore.setLegend()} + />
@@ -223,7 +244,11 @@ const Generator = props => { /> { if (commonStore.templateIsDirty) { commonStore.showConfirm( @@ -234,7 +259,7 @@ const Generator = props => { generatorStore.generate(); } }} - label={`Generoi (${stopCount})`} + label={`Generoi (${generatorStore.component !== 'TerminalPoster' ? stopCount : 1})`} style={{ height: 40, marginLeft: 10 }} primary /> diff --git a/src/components/RadioGroup.js b/src/components/RadioGroup.js index 87b80a7..f3e7332 100644 --- a/src/components/RadioGroup.js +++ b/src/components/RadioGroup.js @@ -14,15 +14,21 @@ const RadioGroup = props => ( label={label} value={props.valuesByLabel[label]} style={{ marginBottom: 10 }} + disabled={props.disabled} /> ))} ); +RadioGroup.defaultProps = { + disabled: false, +}; + RadioGroup.propTypes = { valuesByLabel: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types valueSelected: PropTypes.any.isRequired, // eslint-disable-line react/forbid-prop-types onChange: PropTypes.func.isRequired, + disabled: PropTypes.bool, }; export default RadioGroup; diff --git a/src/components/StopList.js b/src/components/StopList.js index b53201c..be281bc 100644 --- a/src/components/StopList.js +++ b/src/components/StopList.js @@ -74,6 +74,8 @@ const enhance = compose(observer, inject('commonStore', 'generatorStore')); function StopList(props) { const { generatorStore, commonStore } = props; const { rows, checkedRows } = generatorStore; + const { showOnlyCheckedStops, setShowOnlyCheckedStops } = commonStore; + const { stopFilter, setStopFilter } = commonStore; const renderer = rowRenderer(rows, checkedRows, props.onCheck); return ( @@ -82,14 +84,14 @@ function StopList(props) { commonStore.setStopFilter(value)} - value={commonStore.stopFilter} + onChange={(event, value) => setStopFilter(value)} + value={stopFilter} hintText="Suodata..." fullWidth /> - {commonStore.stopFilter && ( + {stopFilter && ( commonStore.setStopFilter('')} + onClick={() => setStopFilter('')} style={{ position: 'absolute', right: 0 }}> @@ -104,10 +106,17 @@ function StopList(props) { /> props.onCheck(rows, true)} label="Valitse kaikki" /> + { + setShowOnlyCheckedStops(!showOnlyCheckedStops); + setStopFilter(''); + }} + label={!showOnlyCheckedStops ? 'Näytä valitut' : 'Näytä kaikki'} + />
diff --git a/src/components/TerminalSelect.js b/src/components/TerminalSelect.js new file mode 100644 index 0000000..9a49266 --- /dev/null +++ b/src/components/TerminalSelect.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { observer, PropTypes as mobxPropTypes } from 'mobx-react'; + +import styled from 'styled-components'; +import Select from 'react-select'; + +const SectionHeading = styled.h4` + margin-bottom: 0.5rem; +`; + +const TerminalSelect = props => { + const mappedTerminals = props.terminals.map(t => ({ + option: t.terminalId, + label: `${t.nameFi} (${t.terminalId})`, + })); + + const selected = mappedTerminals.find(t => t.option === props.selectedTerminal); + + return ( +
+ Terminaali +