diff --git a/.env b/.env index e38635d..e249c5f 100644 --- a/.env +++ b/.env @@ -1,4 +1,3 @@ DAPLA_TEAM_API_URL= DAPLA_CTRL_ADMIN_GROUPS= DAPLA_CTRL_DOCUMENTATION_URL= -DAPLA_CTRL_DAPLA_START_URL= \ No newline at end of file diff --git a/README.md b/README.md index 5e5607d..60ffa6a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -

Dapla ctrl

+

Dapla Ctrl

A web interface for performing administrative tasks related to Dapla teams. @@ -12,93 +12,67 @@

- -
- Table of Contents -
    -
  1. - About The Project - -
  2. -
  3. - Getting Started - -
  4. -
  5. Usage
  6. -
  7. Contributing
  8. -
  9. License
  10. -
-
- ## About The Project -A web interface for performing administrative tasks related to Dapla teams. TODO: Put more info here. - -

(back to top)

- -### Built With - -- [![Vite][Vite.js]][Vite-url] -- [![React][React.js]][React-url] - -

(back to top)

+[![Vite][Vite.js]][Vite-url] +[![React][React.js]][React-url] - +A web interface for performing administrative tasks related to Dapla teams which +supports things like displaying Dapla team members, adding team members and creating new teams. -## Getting Started +## Developing -This is an example of how you may give instructions on setting up your project locally. -To get a local copy up and running follow these simple example steps. - -### Prerequisites - -This is an example of how to list things you need to use the software and how to install them. - -- npm - ```sh - npm install npm@latest -g - ``` -- Install nodemon (required to run the development server) - ``` - npm install -g nodemon - ``` -- If you want to test against local version of dapla-team-api-redux. [Click here for step by step guide to set it up](https://example.com) -- Create .env.local (note you must replace dummy names with real values) - If testing with local version of dapla-team-api-redux put this: - ```sh - touch .env.local && printf 'VITE_DAPLA_TEAM_API_URL="http://localhost:8080"\nVITE_JWKS_URI="https://your-keycloak.domain.com/auth/realms/ssb/protocol/openid-connect/certs"\nVITE_SSB_BEARER_URL="https://your-http-bin.domain.com/bearer"' >> .env.local - ``` - If testing with dapla-team-api-redux in production, put this: - ```sh - touch .env.local && printf 'VITE_DAPLA_TEAM_API_URL="http://your-running-application.domain.com"\nVITE_JWKS_URI="https://your-keycloak.domain.com/auth/realms/ssb/protocol/openid-connect/certs"\nVITE_SSB_BEARER_URL="https://your-http-bin.domain.com/bearer"' >> .env.local - ``` +In Dapla Ctrl we use [effect](https://effect.website) as the standard library for typescript as it bring with it some +powerful primitives for managing asynchrounous effects in a sane way with strong observability support. Furthermore, it provides schema validation for data and a complete set of funmany handy utilityctions for manipulating data in an immutable manner. Prefer writing in a functional style using `effect` when developing Dapla Ctrl. -### Installation +## Setup -1. Clone the repo +1. Clone the repo using tools like `git clone` or `gh repo clone`. +2. Navigate into the repository root directory ```sh - git clone https://github.com/statisticsnorway/dapla-ctrl.git + cd dapla-ctrl ``` -2. Navigate into the repository +3. Start the Nix development environment ```sh - cd dapla-ctrl + nix develop ``` -3. Install NPM packages +4. Install NPM packages ```sh npm install ``` -4. Start the development server and access the application at http://localhost:3000 +5. Start the development server and access the application at http://localhost:3000 ```sh npm run dev ``` +### Note about local development + +Dapla Ctrl assumes all requests include an authorization header when sending requests to the API. Therefore, when developing locally you will need to have a browser plugin that modifies the header with your bearer token. For example you can use [header editor](https://addons.mozilla.org/en-US/firefox/addon/header-editor) for firefox. Add a new rule which matches the URL for the development server and add an authorization header with `Bearer `, don't forget the space between "Bearer" and the token. + +![Screenshot showing how to modify request headers in a browser extension](docs/images/modify_header.png) + +### Tips + +You can use [direnv](https://github.com/direnv/direnv) to automatically hook into your nix shell environment +when `cd`ing into the project's root directory. There also exists plugins like [direnv for vscode](https://marketplace.visualstudio.com/items?itemName=mkhl.direnv) for code editors to hook into this +environment as well. + +If you don't want to use the Nix development environment you have to follow these extra manual steps: + +- Install nodemon (required to run the development server) + + ```sh + npm install -g nodemon + ``` + +- Set environment variables needed by the application: + + ```sh + touch .env.local && printf 'DAPLA_TEAM_API_URL=https://dapla-team-api-v2.staging-bip-app.ssb.no\nPORT=3000\nDAPLA_CTRL_ADMIN_GROUPS=dapla-stat-developers,dapla-skyinfra-developers,dapla-utvik-developers\nDAPLA_CTRL_DOCUMENTATION_URL=https://statistics-norway.atlassian.net/wiki/x/EYC24g' >> .env.local + ``` + ### ESLint and Prettier For ensuring code consistency and adhering to coding standards, our project utilizes ESLint and Prettier. To view linting warnings and errors in the console, it's recommended to run the following script during development: @@ -113,7 +87,7 @@ To automatically fix linting and formatting issues across all files, you can use npm run lint:fix && npm run lint:format ``` -### Integrated Development Environments (IDEs) Support +### IDE Support For seamless integration with popular IDEs such as Visual Studio Code and IntelliJ, consider installing the following plugins: @@ -136,32 +110,7 @@ For seamless integration with popular IDEs such as Visual Studio Code and Intell By incorporating these plugins into your development environment, you can take full advantage of ESLint and Prettier to maintain code quality and consistent formatting throughout your project.

(back to top)

- - - -## Contributing - -Any contributions you make are **greatly appreciated**. - -If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". -Don't forget to give the project a star! Thanks again! - -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/amazing-feature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -

(back to top)

- - - -## License - -Distributed under the MIT License. See `LICENSE.txt` for more information. - -

(back to top)

- + @@ -173,7 +122,7 @@ Distributed under the MIT License. See `LICENSE.txt` for more information. [stars-url]: https://github.com/github_username/repo_name/stargazers [issues-shield]: https://img.shields.io/github/issues/github_username/repo_name.svg?style=for-the-badge [issues-url]: https://github.com/github_username/repo_name/issues -[Vite.js]: https://avatars.githubusercontent.com/u/65625612?s=48&v=4 +[Vite.js]: https://img.shields.io/badge/Vite-2023A?style=for-the-badge&logo=vite&logoColor=61DAFB [Vite-url]: https://vitejs.dev/ [React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB [React-url]: https://reactjs.org/ diff --git a/docs/images/modify_header.png b/docs/images/modify_header.png new file mode 100644 index 0000000..b1bd8e0 Binary files /dev/null and b/docs/images/modify_header.png differ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4bdb8ce --- /dev/null +++ b/flake.lock @@ -0,0 +1,58 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1717285511, + "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1718160348, + "narHash": "sha256-9YrUjdztqi4Gz8n3mBuqvCkMo4ojrA6nASwyIKWMpus=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "57d6973abba7ea108bac64ae7629e7431e0199b6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1717284937, + "narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4242c36 --- /dev/null +++ b/flake.nix @@ -0,0 +1,31 @@ +{ + description = "Development environment for Dapla Ctrl"; + + inputs = { + flake-parts.url = "github:hercules-ci/flake-parts"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = inputs @ {flake-parts, ...}: + flake-parts.lib.mkFlake {inherit inputs;} { + systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin"]; + perSystem = {pkgs, ...}: { + devShells.default = pkgs.mkShell { + shellHook = '' + export DAPLA_TEAM_API_URL=https://dapla-team-api-v2.staging-bip-app.ssb.no + export PORT=3000 + export DAPLA_CTRL_ADMIN_GROUPS=dapla-stat-developers,dapla-skyinfra-developers,dapla-utvik-developers + export DAPLA_CTRL_DOCUMENTATION_URL=https://statistics-norway.atlassian.net/wiki/x/EYC24g + ''; + packages = with pkgs; [ + nixd + nodejs + nodePackages.nodemon + nodePackages.typescript-language-server + pandoc + ]; + }; + formatter = pkgs.alejandra; + }; + }; +} diff --git a/src/pages/CreateTeamForm/CreateTeamForm.tsx b/src/pages/CreateTeamForm/CreateTeamForm.tsx index 3bbb010..818d98c 100644 --- a/src/pages/CreateTeamForm/CreateTeamForm.tsx +++ b/src/pages/CreateTeamForm/CreateTeamForm.tsx @@ -8,6 +8,7 @@ import { Dropdown, Glossary, Input, + Link, Text, TextArea, } from '@statisticsnorway/ssb-component-library' @@ -16,11 +17,12 @@ import { Skeleton } from '@mui/material' import { useEffect, useState, useMemo } from 'react' import { Array as A, Console, Effect, Option as O, pipe } from 'effect' -import FormSubmissionResult from './FormSubmissionResult.tsx' +import FormSubmissionResult, { FormSubmissionResultProps } from './FormSubmissionResult.tsx' import PageLayout from '../../components/PageLayout/PageLayout' import * as Klass from '../../services/klass' import { AutonomyLevel, CreateTeamRequest, createTeam } from '../../services/createTeam' import { User } from '../../@types/user' +import * as Utils from '../../utils/utils.ts' interface DisplayAutonomyLevel { id: AutonomyLevel @@ -38,26 +40,26 @@ interface FormError { errorMessage: string } -interface FormSubmissionResult { - success: boolean - message: string -} - const CreateTeamForm = () => { const uniformNameLengthLimit = 17 // TODO: These should be fetched from the dapla-team-api instead of being hardcoded const teamAutonomyLevels: DisplayAutonomyLevel[] = [ { id: 'managed', title: 'Managed' }, { id: 'semi-managed', title: 'Semi-Managed' }, - { id: 'autonomous', title: 'Autonomous' }, + { id: 'autonomous', title: 'Self-Managed' }, ] - const teamNameGlossaryExplanation = ` - Teamets navn (for eksempel: "Pålegg Brunost"). Dette kan endres senere. - ` + const teamNameGlossaryExplanation = + 'Teamets navn i et lesevennlig format. Navnet bør bestå av et hoveddomenet og et subdomenet, f.eks. "Skatt Næring" og det er tillatt med mellomrom og norske tegn (Æ, Ø, Å).' const uniformNameGlossaryExplanation = - 'Det tekniske teamnavnet som brukes internt i IT-systemene. Her er det flere restriksjoner på hvilke tegn som kan brukes.' + 'Teamets navn i et maskinvennlig format som bl.a. benyttes i filstier til lagringsbøtter. Det er ikke tillatt med mellomrom og norske tegn (Æ, Ø, Å).' + + const sectionGlossaryExplanation = 'Ansvarlig seksjon for teamet.' + + const autonomyLevelGlossaryExplanation = + 'Nivå av frihet et team har til å definere sin egen infrastruktur. Statistikkproduserende team er vanligvis i kategorien "Managed", mens IT-team er "Self-Managed".' - const sectionGlossaryExplanation = 'SSB seksjonen som teamet tilhører.' + const additionalInformationGlossaryExplanation = + 'Informasjon som kan være nyttig for den som oppretter teamet. F.eks. kan man liste opp hvem som skal legges i tilgangsgruppene data-admins og developers her.' const displayNameLabel = 'Visningsnavn' const [displayName, setDisplayName] = useState('') @@ -71,7 +73,7 @@ const CreateTeamForm = () => { const [sections, setSections] = useState([]) const [selectedSection, setSelectedSection] = useState>(O.none()) - const [userName, setUserName] = useState>(O.none) + const [user, setUser] = useState>(O.none) const [selectedAutonomyLevel, setSelectedAutonomyLevel] = useState(teamAutonomyLevels[0]) @@ -82,13 +84,29 @@ const CreateTeamForm = () => { const missingFieldErrorMessage = 'mangler' const validationErrorMessage = 'har en valideringsfeil' + const resetForm = () => { + setDisplayName('') + setUniformName('') + setOverrideUniformName(false) + setUniformNameErrorMsg('') + setSelectedSection(O.none()) + + setSelectedAutonomyLevel(teamAutonomyLevels[0]) + setAdditionalInformation('') + setSubmitButtonClicked(false) + } + const formErrors: FormError[] = useMemo( () => pipe( [ { guard: displayName === '', field: displayNameLabel, errorMessage: missingFieldErrorMessage }, { guard: uniformName === '', field: uniformNameLabel, errorMessage: missingFieldErrorMessage }, - { guard: '' !== uniformNameErrorMsg, field: uniformNameLabel, errorMessage: validationErrorMessage }, + { + guard: '' !== uniformNameErrorMsg, + field: uniformNameLabel, + errorMessage: validationErrorMessage, + }, { guard: O.isNone(selectedSection), field: sectionLabel, errorMessage: missingFieldErrorMessage }, ], (errors) => A.zipWith(A.range(0, errors.length), errors, (idx, error) => ({ id: idx, ...error })), @@ -99,13 +117,10 @@ const CreateTeamForm = () => { [displayName, uniformName, selectedSection, uniformNameErrorMsg] ) - const [formSubmissionResult, setFormSubmissionResult] = useState>(O.none()) - - useEffect(() => { - if (A.isNonEmptyArray(formErrors)) { - setFormSubmissionResult(O.none()) - } - }, [formErrors]) + const [formSubmissionResult, setFormSubmissionResult] = useState({ + loading: false, + formSubmissionResult: O.none(), + }) useEffect(() => { Effect.gen(function* (_) { @@ -128,25 +143,28 @@ const CreateTeamForm = () => { ) // Setting the selectedSection won't be visible beause of a ssb-component bug: https://github.com/statisticsnorway/ssb-component-library/pull/1111 //const sectionCode = yield* getUserSectionCode(userProfile.principal_name) - setUserName(O.some(userProfile.display_name)) + setUser(O.some(userProfile)) setSections(sections) //setSelectedSection(A.findFirst(sections, (s) => s.id === sectionCode)) }).pipe(Effect.runPromise) }, []) - const handleSubmit = (event: React.FormEvent) => { + const handleSubmit = (event: Event) => { event.preventDefault() // Only submit the form if no form errors are present if (A.isEmptyArray(formErrors)) { + const userPrincipalName = O.getOrThrow(user).principal_name const req: CreateTeamRequest = { teamDisplayName: displayName, uniformTeamName: uniformName, sectionCode: O.getOrThrow(selectedSection).id.toString(), - additionalInformation: additionalInformation, + additionalInformation: `This PR was created through Dapla Ctrl. Additional information from user ${userPrincipalName}:\n ${additionalInformation}`, autonomyLevel: selectedAutonomyLevel.id, features: [], } + setFormSubmissionResult({ loading: true, formSubmissionResult: O.none() }) + Effect.gen(function* () { const clientResponse = yield* createTeam(req) yield* Console.log('ClientResponse', clientResponse) @@ -168,7 +186,18 @@ const CreateTeamForm = () => { }), Effect.runPromise ) - .then(setFormSubmissionResult) + .then((res: O.Option<{ success: boolean; message: string }>) => { + if ( + Utils.option( + res, + () => false, + (r) => r.success + ) + ) { + resetForm() + } + setFormSubmissionResult({ loading: false, formSubmissionResult: res }) + }) } } @@ -245,12 +274,19 @@ const CreateTeamForm = () => { ) : ( - {`${O.getOrElse(userName, () => 'loading')} blir teamansvarlig for dette teamet. Hvis noen andre skal være ansvarlig kan det oppgis nedenfor.`} + + {`${Utils.option( + user, + () => 'loading', + (u: User) => u.display_name + )} blir teamansvarlig for dette teamet. Hvis noen andre skal være ansvarlig kan det oppgis i feltet `} + Tilleggsinformasjon. + ) const renderContent = () => ( -
+ ) => e.preventDefault()}> {displayNameLabel}} id='display_name' @@ -278,16 +314,39 @@ const CreateTeamForm = () => { onSelect={(section: DisplaySSBSection) => setSelectedSection(O.some(section))} /> + {'Autonomitetsnivå'} +       + + Les mer her + + + } selectedItem={selectedAutonomyLevel} items={teamAutonomyLevels} onSelect={(autonomyLevel: DisplayAutonomyLevel) => setSelectedAutonomyLevel(autonomyLevel)} /> -