-
-
-
-## Getting Started
+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.
-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.
+## Developing
-### Prerequisites
+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.
-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
- ```
-### Installation
+## Setup
-1. Clone the repo
- ```sh
- git clone https://github.com/statisticsnorway/dapla-ctrl.git
- ```
-2. Navigate into the repository
+1. Clone the repo using tools like `git clone` or `gh repo clone`.
+2. Navigate into the repository root directory
```sh
cd dapla-ctrl
```
-3. Install NPM packages
+3. Start the Nix development environment
+ ```sh
+ nix develop
+ ```
+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
```
+
+### 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
@@ -113,7 +100,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 +123,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.
-
-
-
-## 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
-
-
-
+
@@ -173,7 +135,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/flake.lock b/flake.lock
new file mode 100644
index 00000000..4bdb8cec
--- /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 00000000..4242c36c
--- /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;
+ };
+ };
+}
From febb833bfda58559cad295b5d1fb6f17a5fa18bd Mon Sep 17 00:00:00 2001
From: Nicholas Jaunsen <3789764+skykanin@users.noreply.github.com>
Date: Mon, 17 Jun 2024 15:03:00 +0200
Subject: [PATCH 3/5] fix linting errors
---
README.md | 8 +-
src/pages/CreateTeamForm/CreateTeamForm.tsx | 624 ++++++++++----------
2 files changed, 314 insertions(+), 318 deletions(-)
diff --git a/README.md b/README.md
index f8ada4ec..b94aa8bc 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,6 @@ supports things like displaying Dapla team members, adding team members and crea
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.
-
## Setup
1. Clone the repo using tools like `git clone` or `gh repo clone`.
@@ -57,7 +56,7 @@ powerful primitives for managing asynchrounous effects in a sane way with strong
3. Start the Nix development environment
```sh
nix develop
- ```
+ ```
4. Install NPM packages
```sh
npm install
@@ -66,7 +65,7 @@ powerful primitives for managing asynchrounous effects in a sane way with strong
```sh
npm run dev
```
-
+
### Tips
You can use [direnv](https://github.com/direnv/direnv) to automatically hook into your nix shell environment
@@ -80,6 +79,7 @@ If you don't want to use the Nix development environment you have to follow thes
```sh
npm install -g nodemon
```
+
- Set environment variables needed by the application:
```sh
@@ -100,7 +100,7 @@ To automatically fix linting and formatting issues across all files, you can use
npm run lint:fix && npm run lint:format
```
-### IDE Support
+### IDE Support
For seamless integration with popular IDEs such as Visual Studio Code and IntelliJ, consider installing the following plugins:
diff --git a/src/pages/CreateTeamForm/CreateTeamForm.tsx b/src/pages/CreateTeamForm/CreateTeamForm.tsx
index e6f33451..79f63475 100644
--- a/src/pages/CreateTeamForm/CreateTeamForm.tsx
+++ b/src/pages/CreateTeamForm/CreateTeamForm.tsx
@@ -1,15 +1,15 @@
import styles from './createTeamForm.module.scss'
import {
- Button,
- Card,
- CheckboxGroup,
- Dialog,
- Dropdown,
- Glossary,
- Input,
- Text,
- TextArea,
+ Button,
+ Card,
+ CheckboxGroup,
+ Dialog,
+ Dropdown,
+ Glossary,
+ Input,
+ Text,
+ TextArea,
} from '@statisticsnorway/ssb-component-library'
import * as C from '@statisticsnorway/ssb-component-library'
import { Skeleton } from '@mui/material'
@@ -24,342 +24,338 @@ import { User } from '../../@types/user'
import * as Utils from '../../utils/utils.ts'
interface DisplayAutonomyLevel {
- id: AutonomyLevel
- title: string
+ id: AutonomyLevel
+ title: string
}
interface DisplaySSBSection {
- id: number
- title: string
+ id: number
+ title: string
}
interface FormError {
- id: number
- field: string
- errorMessage: string
+ id: number
+ field: string
+ errorMessage: 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: 'Self-Managed' },
- ]
- const teamNameGlossaryExplanation = `
+ 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: 'Self-Managed' },
+ ]
+ 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 = `
+ const uniformNameGlossaryExplanation = `
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 sectionGlossaryExplanation = 'Ansvarlig seksjon for teamet.'
- const autonomyLevelGlossaryExplanation = `
+ 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". Les mer her.
`
- const additionalInformationGlossaryExplanation = `
+ 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('')
-
- const uniformNameLabel = 'Teknisk teamnavn'
- const [uniformName, setUniformName] = useState('')
- const [overrideUniformName, setOverrideUniformName] = useState(false)
- const [uniformNameErrorMsg, setUniformNameErrorMsg] = useState('')
-
- const sectionLabel = 'Eierseksjon'
- const [sections, setSections] = useState([])
- const [selectedSection, setSelectedSection] = useState>(O.none())
-
- const [user, setUser] = useState>(O.none)
-
- const [selectedAutonomyLevel, setSelectedAutonomyLevel] = useState(teamAutonomyLevels[0])
-
- const [additionalInformation, setAdditionalInformation] = useState('')
-
- const [submitButtonClicked, setSubmitButtonClicked] = useState(false)
-
- 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: O.isNone(selectedSection), field: sectionLabel, errorMessage: missingFieldErrorMessage },
- ],
- (errors) => A.zipWith(A.range(0, errors.length), errors, (idx, error) => ({ id: idx, ...error })),
- A.flatMap((mapping) =>
- mapping.guard ? [{ id: mapping.id, field: mapping.field, errorMessage: mapping.errorMessage }] : []
- )
- ),
- [displayName, uniformName, selectedSection, uniformNameErrorMsg]
- )
-
- const [formSubmissionResult, setFormSubmissionResult] = useState({
- loading: false,
- formSubmissionResult: O.none(),
- })
-
- useEffect(() => {
- Effect.gen(function* (_) {
- const sections: DisplaySSBSection[] = yield* _(
- Klass.fetchSSBSectionInformation().pipe(
- Effect.map((sections: Klass.SSBSections) =>
- sections.map((section) => ({ id: section.code, title: `${section.code} - ${section.name}` }))
- )
- )
- )
-
- const storedUserProfile = localStorage.getItem('userProfile')
- const maybeUserProfile: O.Option = O.fromNullable(storedUserProfile).pipe(O.map(JSON.parse))
-
- const userProfile: User = yield* _(
- Effect.try({
- try: () => O.getOrThrow(maybeUserProfile),
- catch: (error) => new Error(`Element not present: ${error}`),
- })
- )
- // 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)
- setUser(O.some(userProfile))
- setSections(sections)
- //setSelectedSection(A.findFirst(sections, (s) => s.id === sectionCode))
- }).pipe(Effect.runPromise)
- }, [])
-
- 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: `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)
- return O.some(
- clientResponse.status !== 200
- ? {
- success: false,
- message: `Det oppstod en feil ved opprettelse av team. Statuskode: ${clientResponse.status}`,
- }
- : { success: true, message: 'Opprettelse av team ble registert.' }
- )
- })
- .pipe(
- Effect.catchTags({
- ResponseError: (error) => Effect.succeed(O.some({ success: false, message: error.message })),
- RequestError: (error) => Effect.succeed(O.some({ success: false, message: error.message })),
- BodyError: (error) =>
- Effect.succeed(
- O.some({ success: false, message: `Failed to parse body: ${error.reason._tag}` })
- ),
- }),
- Effect.runPromise
- )
- .then((res: O.Option<{ success: boolean; message: string }>) => {
- if (
- Utils.option(
- res,
- () => false,
- (r) => r.success
- )
- ) {
- resetForm()
- }
- setFormSubmissionResult({ loading: false, formSubmissionResult: res })
- })
- }
- }
-
- const toggleUniformNameInput = (checkboxes: string[]): void => {
- const isOverridden = checkboxes.includes('override')
- setOverrideUniformName(isOverridden)
- // If the user unselects the override option generate the uniform name
- // based on the display name again and clear all errors.
- if (!isOverridden) {
- setUniformNameErrorMsg('')
- setUniformName(generateUniformName(displayName))
- }
- }
+ const displayNameLabel = 'Visningsnavn'
+ const [displayName, setDisplayName] = useState('')
+
+ const uniformNameLabel = 'Teknisk teamnavn'
+ const [uniformName, setUniformName] = useState('')
+ const [overrideUniformName, setOverrideUniformName] = useState(false)
+ const [uniformNameErrorMsg, setUniformNameErrorMsg] = useState('')
+
+ const sectionLabel = 'Eierseksjon'
+ const [sections, setSections] = useState([])
+ const [selectedSection, setSelectedSection] = useState>(O.none())
+
+ const [user, setUser] = useState>(O.none)
+
+ const [selectedAutonomyLevel, setSelectedAutonomyLevel] = useState(teamAutonomyLevels[0])
+
+ const [additionalInformation, setAdditionalInformation] = useState('')
+
+ const [submitButtonClicked, setSubmitButtonClicked] = useState(false)
+
+ 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: O.isNone(selectedSection), field: sectionLabel, errorMessage: missingFieldErrorMessage },
+ ],
+ (errors) => A.zipWith(A.range(0, errors.length), errors, (idx, error) => ({ id: idx, ...error })),
+ A.flatMap((mapping) =>
+ mapping.guard ? [{ id: mapping.id, field: mapping.field, errorMessage: mapping.errorMessage }] : []
+ )
+ ),
+ [displayName, uniformName, selectedSection, uniformNameErrorMsg]
+ )
+
+ const [formSubmissionResult, setFormSubmissionResult] = useState({
+ loading: false,
+ formSubmissionResult: O.none(),
+ })
+
+ useEffect(() => {
+ Effect.gen(function* (_) {
+ const sections: DisplaySSBSection[] = yield* _(
+ Klass.fetchSSBSectionInformation().pipe(
+ Effect.map((sections: Klass.SSBSections) =>
+ sections.map((section) => ({ id: section.code, title: `${section.code} - ${section.name}` }))
+ )
+ )
+ )
- const generateUniformName = (displayName: string): string =>
- displayName
- .toLowerCase()
- .replaceAll('team ', '')
- .replaceAll('æ', 'ae')
- .replaceAll('ø', 'oe')
- .replaceAll('å', 'aa')
- .slice(0, uniformNameLengthLimit)
- .trim()
- .replaceAll(' ', '-')
-
- const validateUniformName = (name: string): O.Option => {
- const validUniformName = generateUniformName(name)
- if (name.length > uniformNameLengthLimit) {
- return O.some(`Teknisk navn kan ikke være lengre enn ${uniformNameLengthLimit} tegn`)
- } else if (validUniformName !== name) {
- return O.some('Teknisk navn er ugyldig')
- } else {
- return O.none()
- }
- }
+ const storedUserProfile = localStorage.getItem('userProfile')
+ const maybeUserProfile: O.Option = O.fromNullable(storedUserProfile).pipe(O.map(JSON.parse))
- const handleUniformNameInput = (name: string): void => {
- O.match(validateUniformName(name), {
- onNone: () => {
- setUniformNameErrorMsg('')
- setUniformName(name)
- },
- onSome: (errorMsg: string) => setUniformNameErrorMsg(errorMsg),
+ const userProfile: User = yield* _(
+ Effect.try({
+ try: () => O.getOrThrow(maybeUserProfile),
+ catch: (error) => new Error(`Element not present: ${error}`),
})
- }
-
- const renderUniformNameField = () => {
- const label = (
-
- {uniformNameLabel}
-
+ )
+ // 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)
+ setUser(O.some(userProfile))
+ setSections(sections)
+ //setSelectedSection(A.findFirst(sections, (s) => s.id === sectionCode))
+ }).pipe(Effect.runPromise)
+ }, [])
+
+ 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: `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)
+ return O.some(
+ clientResponse.status !== 200
+ ? {
+ success: false,
+ message: `Det oppstod en feil ved opprettelse av team. Statuskode: ${clientResponse.status}`,
+ }
+ : { success: true, message: 'Opprettelse av team ble registert.' }
)
- return !overrideUniformName ? (
-
- {label}
-
-
- ) : (
-
+ })
+ .pipe(
+ Effect.catchTags({
+ ResponseError: (error) => Effect.succeed(O.some({ success: false, message: error.message })),
+ RequestError: (error) => Effect.succeed(O.some({ success: false, message: error.message })),
+ BodyError: (error) =>
+ Effect.succeed(O.some({ success: false, message: `Failed to parse body: ${error.reason._tag}` })),
+ }),
+ Effect.runPromise
)
+ .then((res: O.Option<{ success: boolean; message: string }>) => {
+ if (
+ Utils.option(
+ res,
+ () => false,
+ (r) => r.success
+ )
+ ) {
+ resetForm()
+ }
+ setFormSubmissionResult({ loading: false, formSubmissionResult: res })
+ })
+ }
+ }
+
+ const toggleUniformNameInput = (checkboxes: string[]): void => {
+ const isOverridden = checkboxes.includes('override')
+ setOverrideUniformName(isOverridden)
+ // If the user unselects the override option generate the uniform name
+ // based on the display name again and clear all errors.
+ if (!isOverridden) {
+ setUniformNameErrorMsg('')
+ setUniformName(generateUniformName(displayName))
}
+ }
+
+ const generateUniformName = (displayName: string): string =>
+ displayName
+ .toLowerCase()
+ .replaceAll('team ', '')
+ .replaceAll('æ', 'ae')
+ .replaceAll('ø', 'oe')
+ .replaceAll('å', 'aa')
+ .slice(0, uniformNameLengthLimit)
+ .trim()
+ .replaceAll(' ', '-')
+
+ const validateUniformName = (name: string): O.Option => {
+ const validUniformName = generateUniformName(name)
+ if (name.length > uniformNameLengthLimit) {
+ return O.some(`Teknisk navn kan ikke være lengre enn ${uniformNameLengthLimit} tegn`)
+ } else if (validUniformName !== name) {
+ return O.some('Teknisk navn er ugyldig')
+ } else {
+ return O.none()
+ }
+ }
- const renderTeamOwnerCard = (isLoading: boolean) =>
- isLoading ? (
-
- ) : (
-
-
- {`${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 handleUniformNameInput = (name: string): void => {
+ O.match(validateUniformName(name), {
+ onNone: () => {
+ setUniformNameErrorMsg('')
+ setUniformName(name)
+ },
+ onSome: (errorMsg: string) => setUniformNameErrorMsg(errorMsg),
+ })
+ }
- const renderContent = () => (
-
+ const renderUniformNameField = () => {
+ const label = (
+
+ {uniformNameLabel}
+
+ )
+ return !overrideUniformName ? (
+
+ {label}
+
+
+ ) : (
+
+ )
+ }
+
+ const renderTeamOwnerCard = (isLoading: boolean) =>
+ isLoading ? (
+
+ ) : (
+
+
+ {`${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.
+
+
)
- return
+ const renderContent = () => (
+
+ )
+
+ return
}
export default CreateTeamForm
From 227457e61cd6e61c80d250a954aca6fd695cd3b7 Mon Sep 17 00:00:00 2001
From: Nicholas Jaunsen <3789764+skykanin@users.noreply.github.com>
Date: Mon, 17 Jun 2024 16:14:19 +0200
Subject: [PATCH 4/5] Add link for autonomylevel explanation
---
src/pages/CreateTeamForm/CreateTeamForm.tsx | 34 +++++++++++++--------
1 file changed, 21 insertions(+), 13 deletions(-)
diff --git a/src/pages/CreateTeamForm/CreateTeamForm.tsx b/src/pages/CreateTeamForm/CreateTeamForm.tsx
index 79f63475..818d98c2 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'
@@ -47,22 +48,18 @@ const CreateTeamForm = () => {
{ id: 'semi-managed', title: 'Semi-Managed' },
{ id: 'autonomous', title: 'Self-Managed' },
]
- 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 = `
- 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 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 =
+ '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". Les mer her.
- `
+ 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 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 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('')
@@ -317,7 +314,18 @@ const CreateTeamForm = () => {
onSelect={(section: DisplaySSBSection) => setSelectedSection(O.some(section))}
/>
{'Autonomitetsnivå'}}
+ header={
+
+ {'Autonomitetsnivå'}
+
+
+ Les mer her
+
+
+ }
selectedItem={selectedAutonomyLevel}
items={teamAutonomyLevels}
onSelect={(autonomyLevel: DisplayAutonomyLevel) => setSelectedAutonomyLevel(autonomyLevel)}
From f39cc5036171f09926cc91f6287f07957bb4f511 Mon Sep 17 00:00:00 2001
From: Nicholas Jaunsen <3789764+skykanin@users.noreply.github.com>
Date: Tue, 18 Jun 2024 09:44:21 +0200
Subject: [PATCH 5/5] update README with header modification docs
---
README.md | 25 ++++++-------------------
docs/images/modify_header.png | Bin 0 -> 88635 bytes
2 files changed, 6 insertions(+), 19 deletions(-)
create mode 100644 docs/images/modify_header.png
diff --git a/README.md b/README.md
index b94aa8bc..60ffa6ae 100644
--- a/README.md
+++ b/README.md
@@ -12,25 +12,6 @@
-
-
-
## About The Project
@@ -66,6 +47,12 @@ powerful primitives for managing asynchrounous effects in a sane way with strong
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
diff --git a/docs/images/modify_header.png b/docs/images/modify_header.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1bd8e0d90b62b724e39585b49a48c73998379ee
GIT binary patch
literal 88635
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