diff --git a/README.md b/README.md index 8a3820e..217ca84 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,29 @@ -# Tailormap Starter project +# Tailormap Hello World component -Use this project when you want to extend the Tailormap viewer with extra functionality. You can fork this repository to get started! +This is a demo component that can be imported inside a Tailormap project. It can also serve as example for how to create an external component for Tailormap. -## Public extensions -You can create a fork of this project to create a public repository with your extensions. Remember to [keep your fork in sync](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to get -updates from this repository (this may include dependency or other updates). +Use `ng add @tailormap-b3p/hello-world` to use the component inside Tailormap -## Closed source extensions -The license of this project and dependencies allows closed-source modifications and extensions. You can create a new _private_ repository -using this as a template. To keep your private repository in sync, add a new Git remote to this repository and merge changes from the -remote. Note that even if you create a private repository, if you deploy the result online anyone can see and try to de-obfuscate the -JavaScript source code and TypeScript source maps are available by default (you can change that in angular.json). +Inside the projects/hello-world directory the code for this library is located. -## Keeping in sync +In `hello-world.module.ts` we register our components to Tailormap +In `hello-world-panel.component.ts` we register our menu button in Tailormap to toggle our panel -You can keep in sync by following new releases of the NPM packages of the Angular frontend, using [dependabot](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/about-dependabot-version-updates) -for example. To keep up-to-date with the Tailormap API, change the version of the tailormap-api docker base image. The default Tailormap API -version in the Dockerfile is set to 'snapshot', which is updated on each commit to the main branch. You may want to set this to a released -version and use something like [Renovate](https://www.mend.io/renovate/) to keep up-to-date with new releases. +Schematics are added to be able to install the library using `ng add` -## Developing extensions +# Publishing -See [tailormap-viewer](https://github.com/B3Partners/tailormap-viewer/) for details. - -See [GETTING_STARTED](docs/GETTING_STARTED.md) to find an example for adding a component to Tailormap. - -## Development - -See the documentation of [tailormap-viewer](https://github.com/B3Partners/tailormap-viewer/) for how to run a development server, use a Tailormap API backend, etc. - -## Building a Docker image - -Choose a container name for your customized Tailormap and the API version you want to use. If you use `snapshot` or `latest` (tags that get -updated with newer images) you'll want to `docker pull` that image first: - -Build your Docker image as follows: +The component can be published by using the following commands: +```shell +cd projects/hello-world +npm version patch +cd ../.. +npm run build-hello-world +cd dist/hello-world +npm publish --scope=@tailormap-b3p --registry=https://repo.b3p.nl/nexus/repository/npm-public +cd ../.. +git add -A ``` -docker pull ghcr.io/b3partners/tailormap-api:snapshot -docker build --build-arg API_VERSION=snapshot -t my-organisation/tailormap-my-custom-version:snapshot . -``` - -You may want to use other values for `API_VERSION` to use a specific Tailormap API version and the image tag. Make sure the Tailormap API -version matches what the NPM packages you use in this Angular app expect. - -You can run your customized Tailormap container separately or using the Docker Compose configuration in tailormap-viewer. For Docker -Compose, specify your custom image and tag in the `TAILORMAP_IMAGE` and `VERSION` variables in an environment file (see the README of -tailormap-viewer and its `.env.template` for details). - -# Setting up continuous deployment - -To add continuous deployment, you need a server with Docker and Traefik configured with `--providers.docker` and a Let's Encrypt certificate -resolver named `letsencrypt`. Generate an SSH keypair and add the public key to the `~/.ssh/authorized_keys` file for an account that has -Docker access on your server. Assign a hostname for the deployments to this server. - -You can use different SSH keypairs for different deployments. Just add more public keys to the `authorized_keys` file. - -Add the repository variables below in GitHub to enable continuous deployment. - -Like the continuous deployment in `tailormap-viewer`, the Tailormap API backend will only be deployed for the `main` branch and pull request -deployments will only serve the static Angular frontend on a different base path which will use the API for the main deployment on the `/api` -path. The deployments will be added to the GitHub environment named 'test'. - -- `DEPLOY`: set to `true` -- `DEPLOY_HOSTNAME`: the hostname where Tailormap should run on, which points to the server -- `DEPLOY_PROJECT_NAME`: name of your customized project, used for docker image and container name (a-z) -- `ADMIN_HASHED_PASSWORD`: Hashed password of the `tm-admin` account created when deploying the first time -- `DEPLOY_IMAGE_TAG`: Tag for Docker image (without version), for example `ghcr.io/b3partners/tailormap-starter`. The image is built in a GitHub Actions worker and uploaded to the server -- it is not pushed to - a registry. The version used is `snapshot` for deployments for the main `branch` and `pr-nn` for pull request deployments. - -Add the following as GitHub secrets: - -- `DEPLOY_DOCKER_HOST`: something like `ssh://github-docker-actions@your.server.com` -- `DEPLOY_DOCKER_HOST_SSH_CERT`: the public part of the SSH key as added to `authorized_keys`, something like `ssh-rsa AAAAB3NzaC1yc2EAA(...)ei3Uv4zj9/8M= user@host` -- `DEPLOY_DOCKER_HOST_SSH_KEY`: the private part of the SSH key, without passphrase, something like: - -``` ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAA... -... ------END OPENSSH PRIVATE KEY----- -``` -## GitHub Actions workflow security - -Be sure to check the settings of your repository so that if you accept pull requests not everybody can run a modified workflow file to -exfiltrate the private key and use your server. - -## Logging in after first deployment - -When Tailormap is deployed for the main branch for the first time, the database tables are created and an admin account is created. By -setting the `ADMIN_HASHED_PASSWORD` variable you can configure the password for the `tm-admin` account so you can login to the admin (go to -`/admin/`). You can generate a hash with Spring CLI: ` docker run --rm rocko/spring-boot-cli-docker spring encodepassword "[your password]"`. -You can leave this variable empty but after the first deployment you'll need to check the Tailormap server logs for the account details or -reset the password as described in the README of tailormap-viewer. -When you've successfully logged in to the admin you can start filling the catalog and configuring a Tailormap application. +Or simply by running `node publish.js` diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md deleted file mode 100644 index a15992b..0000000 --- a/docs/GETTING_STARTED.md +++ /dev/null @@ -1,218 +0,0 @@ -# Getting Started -This document describes the first steps to create a custom component and run it inside Tailormap - -### First steps - -First we want to create a new Angular component, so we are running the Angular CLI to generate one - -```bash -npm run ng -- g c logo-on-map -``` - -This will create a new component inside the `projects/app/src/app/logo-on-map` directory - -Next we want to register this component with Tailormap. To do that we need to use one of the entry points that Tailormap provides (see the Tailormap API docs). Here we are adding something on the map, so we will use the `MapControlsService` to register our component. We will add this in the `app.module.ts`. Add the following code to `app.module.ts` inside the `AppModule` class - -``` -constructor( - mapControlsService: MapControlsService, -) { - mapControlsService.registerComponent({ type: 'LOGO_ON_MAP', component: LogoOnMapComponent }); -} -``` - -You will also need to add `MapControlsService` to the `@tailormap-viewer/core` imports - -Your file should now look like: - -```typescript -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; - -import { AppComponent } from './app.component'; -import { CoreModule, CoreRoutingModule, MapControlsService } from "@tailormap-viewer/core"; -import { SharedModule } from "@tailormap-viewer/shared"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { LogoOnMapComponent } from './logo-on-map/logo-on-map.component'; - -@NgModule({ - declarations: [ - AppComponent, - LogoOnMapComponent - ], - imports: [ - BrowserModule, - BrowserAnimationsModule, - CoreModule, - CoreRoutingModule, - SharedModule, - ], - providers: [], - bootstrap: [AppComponent] -}) -export class AppModule { - constructor( - mapControlsService: MapControlsService, - ) { - mapControlsService.registerComponent({ type: 'LOGO_ON_MAP', component: LogoOnMapComponent }); - } -} -``` - -Let's add some styling to our component (`logo-on-map.component.css`): - -```css -p { - position: absolute; - right: 0; - bottom: 0; - margin: 0; - padding: 16px; - white-space: nowrap; - background-color: darkviolet; - color: darkgrey; -} -``` - -Now run the application using `npm run start` and visit `http://localhost:4200` in your browser. If everything went as planned you should see the Tailormap viewer and our new component in the right-bottom corner. - -### Implement your component - -With our custom component we want to show a logo on a map. For that we need coordinates for where to place the logo and convert these coordinates to pixels on the screen, so we can place our logo. - -Tailormap has some useful mapping APIs and here we will use the `getPixelForCoordinates$` method to convert coordinates to pixels. Since it's an `Observable` stream we get updates once we move the map, so we can update the position of the image accordingly. - -Below the source code for our component: - -```typescript -import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { MapService } from "@tailormap-viewer/map"; -import { HttpClient } from "@angular/common/http"; -import { Observable, map, of, switchMap, concatMap } from "rxjs"; -import { default as WKT } from "ol/format/WKT"; -import { Geometry, Point } from "ol/geom"; -import { Feature } from "ol"; - -type LocationServerResponseType = { response: { docs: Array<{ centroide_ll: string }> }}; - -@Component({ - selector: 'app-logo-on-map', - templateUrl: './logo-on-map.component.html', - styleUrls: ['./logo-on-map.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LogoOnMapComponent implements OnInit { - - public imgStyle$: Observable<{ top?: string; left?: string; display: string }> = of({ display: 'none' }); - - constructor( - private mapService: MapService, - private httpClient: HttpClient, - ) { } - - public ngOnInit(): void { - // Lookup the address for B3Partners HQ using the 'PDOK locatieserver' - const url = 'https://geodata.nationaalgeoregister.nl/locatieserver/v3/free?q=Zonnebaan%2012C,%203542%20EC%20Utrecht&rows=1&fl=id,bron,weergavenaam,type,centroide_rd,centroide_ll&fq=*'; - this.imgStyle$ = this.httpClient.get(url) - .pipe( - switchMap(result => { - if (!result.response?.docs || result.response.docs.length === 0) { - return of(null); - } - // Get the current projection we are working in - return this.mapService.getProjectionCode$() - .pipe( - concatMap(projectionCode => { - if (!projectionCode) { - return of(null); - } - // Convert the WKT from the service to geometry using Openlayers - const format = new WKT(); - const feature: Feature = format.readFeature(result.response.docs[0].centroide_ll, { - dataProjection: 'EPSG:4326', - featureProjection: projectionCode - }); - // Use as Point geometry since we know it's a point - const coords = (feature.getGeometry() as Point)?.getCoordinates(); - if (!coords) { - return of(null); - } - // Get screen pixels for coordinates - return this.mapService.getPixelForCoordinates$([coords[0], coords[1]]); - }) - ) - }), - map(coords => { - if (!coords) { - // Oops, no coordinates, hide image - return { display: 'none' }; - } - // CSS styling to display the image on the correct position - return { display: 'inline-block', left: `${coords[0]}px`, top: `${coords[1]}px` }; - }) - ); - } - -} -``` -CSS (logo-on-map.component.css): -```css -img { - position: absolute; - width: 30px; - height: 30px; - display: none; -} -``` -and template (logo-on-map.component.html) -```angular2html -Logo B3Partners -``` -The code should be pretty self-explanatory if you are familiar with the Angular framework. But basically we use Angulars httpCLient to fetch the location, convert the WKT returned from the service to coordinates using Openlayers and the `getProjectionCode$` method from Tailormap and then convert these coordinates to pixels using `getPixelForCoordinates$` - -If we run the application now, we should be able to see the B3Partners logo placed on the right location on the map. - -#### Testing - -Let's add a simple test to check if our code is working correctly. We are using the Angular Testing Library framework to make testing easier and more readable. Add the following code to `logo-on-map.component.spec.ts` - -```typescript -import { render, screen } from '@testing-library/angular'; -import { LogoOnMapComponent } from './logo-on-map.component'; -import { of } from "rxjs"; -import { MapService } from "@tailormap-viewer/map"; -import { HttpClient } from "@angular/common/http"; - -describe('LogoOnMapComponent', () => { - - test('should render', async () => { - const httpClient = { - get: jest.fn(() => of({ - response: { - docs: [ - { centroide_ll: 'POINT(5.04185307 52.11887446)' }, - ], - }, - })), - }; - const mapService = { - getProjectionCode$: jest.fn(() => of('EPSG:28992')), - getPixelForCoordinates$: jest.fn(() => of([ 5, 5 ])), - } - await render(LogoOnMapComponent, { - providers: [ - { provide: MapService, useValue: mapService }, - { provide: HttpClient, useValue: httpClient }, - ] - }); - const img = await screen.getByAltText('Logo B3Partners'); - expect((img as HTMLElement).style.left).toEqual('5px'); - expect((img as HTMLElement).style.top).toEqual('5px'); - }); - -}); -``` -Running the tests `npm run test` should give us all green results.