From d73e5bd99cb3d6c473a1c2048abd0a2d9c5cc84a Mon Sep 17 00:00:00 2001
From: Bjarke Madsen <bjarke@nordu.net>
Date: Fri, 24 Jan 2025 14:05:29 +0100
Subject: [PATCH] Linting & pass new eslint config

---
 .../src/components/ColorBadgeService.tsx           |  4 ++--
 .../src/components/ScrollableMatrix.tsx            |  4 ++--
 .../src/components/ScrollableTable.tsx             |  2 +-
 compendium-frontend/src/helpers/charthelpers.tsx   |  2 +-
 compendium-frontend/src/helpers/dataconversion.tsx | 12 ++++++------
 compendium-frontend/src/matomo/MatomoTracker.ts    |  1 -
 compendium-frontend/src/matomo/UseMatomo.ts        |  2 +-
 .../src/pages/Network/IRUDuration.tsx              |  1 -
 .../src/pages/Network/NetworkMapUrls.tsx           |  2 +-
 .../src/pages/Network/TrafficUrl.tsx               |  2 +-
 .../src/pages/Network/TrafficVolume.tsx            |  4 ++--
 .../src/pages/Network/WeatherMap.tsx               |  2 +-
 .../pages/Standards&Policies/CorporateStrategy.tsx |  2 +-
 .../src/pages/Standards&Policies/Policy.tsx        |  2 +-
 .../src/providers/ConfigProvider.tsx               |  4 ++--
 compendium-frontend/src/providers/NrenProvider.tsx |  2 +-
 compendium-frontend/src/survey/Landing.tsx         |  2 +-
 compendium-frontend/src/survey/ShowUser.tsx        |  2 +-
 .../src/survey/SurveyContainerComponent.tsx        | 14 +++++++-------
 .../src/survey/SurveyNavigationComponent.tsx       |  1 -
 compendium-frontend/src/survey/api/survey.ts       |  2 +-
 .../survey/management/UserManagementComponent.tsx  |  4 ++--
 .../src/survey/validation/validation.ts            |  2 +-
 23 files changed, 36 insertions(+), 39 deletions(-)

diff --git a/compendium-frontend/src/components/ColorBadgeService.tsx b/compendium-frontend/src/components/ColorBadgeService.tsx
index 73a3df0f..c978a92e 100644
--- a/compendium-frontend/src/components/ColorBadgeService.tsx
+++ b/compendium-frontend/src/components/ColorBadgeService.tsx
@@ -7,8 +7,8 @@ function ColorBadgeService({ year, active, serviceInfo, tickServiceIndex, curren
   let tooltip_text = "No additional information available";
 
   if (serviceInfo !== undefined) {
-    let serviceName = serviceInfo['service_name']
-    let year = serviceInfo['year']
+    const serviceName = serviceInfo['service_name']
+    const year = serviceInfo['year']
     let name = serviceInfo['product_name'];
     let desc = serviceInfo['official_description'];
     let info = serviceInfo['additional_information'];
diff --git a/compendium-frontend/src/components/ScrollableMatrix.tsx b/compendium-frontend/src/components/ScrollableMatrix.tsx
index 14a0060c..2a292611 100644
--- a/compendium-frontend/src/components/ScrollableMatrix.tsx
+++ b/compendium-frontend/src/components/ScrollableMatrix.tsx
@@ -10,7 +10,7 @@ const CELL_SIZE = 8
 interface ScrollableMatrixProps {
 
     // dataLookup is a map of NRENs, years, and categories that maps to data for that category in that year for that NREN
-    dataLookup: Map<string, Map<number, Map<string, { [key: string]: any }>>>;
+    dataLookup: Map<string, Map<number, Map<string, { [key: string]: { [key: string]: string|number } }>>>;
 
     // rowInfo is a map of row titles and the lookup key that maps to data for that row
     rowInfo: { [key: string]: string };
@@ -34,7 +34,7 @@ export function ScrollableMatrix({ dataLookup, rowInfo, categoryLookup, isTickIc
 
             Array.from(dataLookup.entries()).sort(
                 ([nrenA], [nrenB]) => nrenA.localeCompare(nrenB)
-            ).forEach(([nren, nrenData]) => {
+            ).forEach(([_nren, nrenData]) => {
                 nrenData.forEach((yearData) => {
                     const valuesForCategory = yearData.get(categoryKey);
                     if (!valuesForCategory) return;
diff --git a/compendium-frontend/src/components/ScrollableTable.tsx b/compendium-frontend/src/components/ScrollableTable.tsx
index 549b135f..3b31659a 100644
--- a/compendium-frontend/src/components/ScrollableTable.tsx
+++ b/compendium-frontend/src/components/ScrollableTable.tsx
@@ -23,7 +23,7 @@ export function ScrollableTable<T extends NrenAndYearDatapoint>({ dataLookup, co
         return (
             <CollapsibleBox title={nren} key={nren} theme="-table" startCollapsed>
                 <div className="scrollable-horizontal">
-                    {Array.from(nrenData.entries()).map(([year, yearData], index) => {
+                    {Array.from(nrenData.entries()).map(([year, yearData]) => {
                         // workaround for setting the background color of the ::before element to the color of the year
                         const style = { 
                             '--before-color': `var(--color-of-the-year-muted-${year % 9})`,
diff --git a/compendium-frontend/src/helpers/charthelpers.tsx b/compendium-frontend/src/helpers/charthelpers.tsx
index face2f1c..941da39a 100644
--- a/compendium-frontend/src/helpers/charthelpers.tsx
+++ b/compendium-frontend/src/helpers/charthelpers.tsx
@@ -6,7 +6,7 @@ interface options {
     tooltipPrefix?: string;
     tooltipUnit?: string;
     tickLimit?: number;
-    valueTransform?: (value: any) => number | string;
+    valueTransform?: (value) => number | string;
 }
 export const getLineChartOptions = ({ title, unit, tooltipPrefix, tooltipUnit, tickLimit, valueTransform }: options) => {
     return {
diff --git a/compendium-frontend/src/helpers/dataconversion.tsx b/compendium-frontend/src/helpers/dataconversion.tsx
index 73b95e06..df93348e 100644
--- a/compendium-frontend/src/helpers/dataconversion.tsx
+++ b/compendium-frontend/src/helpers/dataconversion.tsx
@@ -21,7 +21,7 @@ const stringToColour = function (str) {
     return colour;
 }
 
-export function addTooltip<T extends NrenAndYearDatapoint>(dataLookup: Map<string, Map<string, Map<number, T>>>, processTooltip = (column: string, datapoint: T): string | undefined => undefined) {
+export function addTooltip<T extends NrenAndYearDatapoint>(dataLookup: Map<string, Map<string, Map<number, T>>>, processTooltip = (_column: string, _datapoint: T): string | undefined => undefined) {
     const withTooltip = new Map<string, Map<string, Map<number, { [key: string]: string | number }>>>();
 
     for (const [nren, nrenMap] of dataLookup) {
@@ -193,7 +193,7 @@ export function createCategoryMatrixLookup<T extends NrenAndYearDatapoint>(
     */
 
     // Row identifier -> NREN -> Year -> Data for that year
-    const dataLookup = new Map<string, Map<number, Map<string, { [key: string]: any }>>>();
+    const dataLookup = new Map<string, Map<number, Map<string, { [key: string]: { [key: string]: string|number } }>>>();
 
     const processMatrixLookup = (data: T[], categoryField: keyof T | undefined, field: string) => {
         data.forEach(datapoint => {
@@ -207,8 +207,8 @@ export function createCategoryMatrixLookup<T extends NrenAndYearDatapoint>(
             const nren = datapoint.nren;
             const year = datapoint.year;
 
-            const nrenData = dataLookup.get(nren) || new Map<number, Map<string, { [key: string]: any }>>(); // NREN -> Year -> Data for that year
-            const yearData = nrenData.get(year) || new Map<string, { [key: string]: any }>(); // Year -> Data for that year
+            const nrenData = dataLookup.get(nren) || new Map<number, Map<string, { [key: string]: string|number }>>(); // NREN -> Year -> Data for that year
+            const yearData = nrenData.get(year) || new Map<string, { [key: string]: { [key: string]: string|number } }>(); // Year -> Data for that year
             const values = yearData.get(rowIdentifier as string) || {}; // Data for that year
 
             const value = datapoint[field];
@@ -635,7 +635,7 @@ export const createNRENStaffDatasetAbsolute = (data: NrenStaff[], selectedYears:
         return nrenA.localeCompare(nrenB);
     });
 
-    function getDataset(year, index) {
+    function getDataset(year, _index) {
         const red = "rgba(219, 42, 76, 1)"
 
         return {
@@ -694,7 +694,7 @@ export const createBarChartDataset = <T extends NrenAndYearDatapoint>(data: T[],
     });
     const labelsYear = [...new Set(data.map((item) => item.year))].sort();
 
-    function getDataset(year, index) {
+    function getDataset(year, _index) {
         const red = "rgba(219, 42, 76, 1)"
 
         return {
diff --git a/compendium-frontend/src/matomo/MatomoTracker.ts b/compendium-frontend/src/matomo/MatomoTracker.ts
index 645c598d..1427781f 100644
--- a/compendium-frontend/src/matomo/MatomoTracker.ts
+++ b/compendium-frontend/src/matomo/MatomoTracker.ts
@@ -69,7 +69,6 @@ class MatomoTracker {
 
         Object.entries(configurations).forEach(([name, instructions]) => {
             if (instructions instanceof Array) {
-                // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
                 this.pushInstruction(name, ...instructions)
             } else {
                 this.pushInstruction(name, instructions)
diff --git a/compendium-frontend/src/matomo/UseMatomo.ts b/compendium-frontend/src/matomo/UseMatomo.ts
index a5f432e2..13a7cd0b 100644
--- a/compendium-frontend/src/matomo/UseMatomo.ts
+++ b/compendium-frontend/src/matomo/UseMatomo.ts
@@ -32,7 +32,7 @@ const trackEvent = useCallback(
 
     const pushInstruction = useCallback(
         (name: string, ...args: any[]) => { // eslint-disable-line @typescript-eslint/no-explicit-any
-            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+             
             instance?.pushInstruction(name, ...args)
         },
         [instance],
diff --git a/compendium-frontend/src/pages/Network/IRUDuration.tsx b/compendium-frontend/src/pages/Network/IRUDuration.tsx
index d374400a..bcc8df07 100644
--- a/compendium-frontend/src/pages/Network/IRUDuration.tsx
+++ b/compendium-frontend/src/pages/Network/IRUDuration.tsx
@@ -1,5 +1,4 @@
 import React, { ReactElement, useContext } from 'react';
-import { Row } from "react-bootstrap";
 import { Line } from 'react-chartjs-2';
 
 
diff --git a/compendium-frontend/src/pages/Network/NetworkMapUrls.tsx b/compendium-frontend/src/pages/Network/NetworkMapUrls.tsx
index 818f4514..eb85dea4 100644
--- a/compendium-frontend/src/pages/Network/NetworkMapUrls.tsx
+++ b/compendium-frontend/src/pages/Network/NetworkMapUrls.tsx
@@ -12,7 +12,7 @@ import NrenYearTable from "../../components/NrenYearTable";
 
 function NetworkMapUrlPage(): React.ReactElement {
     const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext);
-    const { data, years, nrens } = useData<NetworkMapUrls>('/api/network-map-urls', setFilterSelection);
+    const { data, nrens } = useData<NetworkMapUrls>('/api/network-map-urls', setFilterSelection);
 
     const latestData = data ? getLatestData(data) : [];
     const selectedData = latestData.filter(data =>
diff --git a/compendium-frontend/src/pages/Network/TrafficUrl.tsx b/compendium-frontend/src/pages/Network/TrafficUrl.tsx
index b52de86b..a200cb19 100644
--- a/compendium-frontend/src/pages/Network/TrafficUrl.tsx
+++ b/compendium-frontend/src/pages/Network/TrafficUrl.tsx
@@ -12,7 +12,7 @@ import NrenYearTable from "../../components/NrenYearTable";
 
 function TrafficUrlPage(): React.ReactElement {
     const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext);
-    const { data, years, nrens } = useData<TrafficStatistics>('/api/traffic-stats', setFilterSelection);
+    const { data, nrens } = useData<TrafficStatistics>('/api/traffic-stats', setFilterSelection);
 
     const latestData = data ? getLatestData(data) : [];
     const selectedData = latestData.filter(data =>
diff --git a/compendium-frontend/src/pages/Network/TrafficVolume.tsx b/compendium-frontend/src/pages/Network/TrafficVolume.tsx
index 08f2c1df..8bbb60ff 100644
--- a/compendium-frontend/src/pages/Network/TrafficVolume.tsx
+++ b/compendium-frontend/src/pages/Network/TrafficVolume.tsx
@@ -37,7 +37,7 @@ ChartJS.register(
 function TrafficVolumePage(): ReactElement {
 
     const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext);
-    const { data, years, nrens } = useData<TrafficVolume>('/api/traffic-volume', setFilterSelection);
+    const { data, nrens } = useData<TrafficVolume>('/api/traffic-volume', setFilterSelection);
 
     const selectedData = data.filter(data =>
         filterSelection.selectedNrens.includes(data.nren) // we only allow filtering nrens for this page
@@ -65,7 +65,7 @@ function TrafficVolumePage(): ReactElement {
     return (
         <DataPage title="NREN Traffic - NREN Customers & External Networks"
             description={<span>The four graphs below show the estimates of total annual traffic in PB (1000 TB) to & from NREN customers,
-                and to & from external networks. NREN customers are taken to mean sources that are part of the NREN's connectivity remit,
+                and to & from external networks. NREN customers are taken to mean sources that are part of the NREN&apos;s connectivity remit,
                 while external networks are understood as outside sources including GÉANT, the general/commercial internet, internet
                 exchanges, peerings, other NRENs, etc.</span>} category={Sections.Network} filter={filterNode}
             data={selectedData} filename="NREN_traffic_estimates_data">
diff --git a/compendium-frontend/src/pages/Network/WeatherMap.tsx b/compendium-frontend/src/pages/Network/WeatherMap.tsx
index 51940665..54029af8 100644
--- a/compendium-frontend/src/pages/Network/WeatherMap.tsx
+++ b/compendium-frontend/src/pages/Network/WeatherMap.tsx
@@ -12,7 +12,7 @@ import NrenYearTable from "../../components/NrenYearTable";
 
 function NetworkWeatherMapPage(): React.ReactElement {
     const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext);
-    const { data: fetchedData, years, nrens } = useData<WeatherMap>('/api/weather-map', setFilterSelection);
+    const { data: fetchedData, nrens } = useData<WeatherMap>('/api/weather-map', setFilterSelection);
 
     const latestData = fetchedData ? getLatestData(fetchedData) : [];
     const selectedData = latestData.filter(data =>
diff --git a/compendium-frontend/src/pages/Standards&Policies/CorporateStrategy.tsx b/compendium-frontend/src/pages/Standards&Policies/CorporateStrategy.tsx
index 11c3f0dd..33266877 100644
--- a/compendium-frontend/src/pages/Standards&Policies/CorporateStrategy.tsx
+++ b/compendium-frontend/src/pages/Standards&Policies/CorporateStrategy.tsx
@@ -15,7 +15,7 @@ function CorporateStrategyPage() {
     const validityCheck = (data: CorporateStrategy) => !!data[dataField];
 
     const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext);
-    const { data, years, nrens } = useData<CorporateStrategy>('/api/policy', setFilterSelection, validityCheck);
+    const { data, nrens } = useData<CorporateStrategy>('/api/policy', setFilterSelection, validityCheck);
 
     const policyData = data ? getLatestData(data) : [];
 
diff --git a/compendium-frontend/src/pages/Standards&Policies/Policy.tsx b/compendium-frontend/src/pages/Standards&Policies/Policy.tsx
index 63ab52c2..a4b4d294 100644
--- a/compendium-frontend/src/pages/Standards&Policies/Policy.tsx
+++ b/compendium-frontend/src/pages/Standards&Policies/Policy.tsx
@@ -13,7 +13,7 @@ import NrenYearTable from '../../components/NrenYearTable';
 
 function PolicyPage() {
     const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext);
-    const { data: fetchedData, years, nrens } = useData<Policy>('/api/policy', setFilterSelection);
+    const { data: fetchedData, nrens } = useData<Policy>('/api/policy', setFilterSelection);
 
     const policyData = fetchedData ? getLatestData(fetchedData) : [];
 
diff --git a/compendium-frontend/src/providers/ConfigProvider.tsx b/compendium-frontend/src/providers/ConfigProvider.tsx
index 65057d38..9ec43033 100644
--- a/compendium-frontend/src/providers/ConfigProvider.tsx
+++ b/compendium-frontend/src/providers/ConfigProvider.tsx
@@ -21,7 +21,7 @@ const saveConfigToLocalStorage = (config) => {
 }
 
 export type BaseConfig = {
-    [K: string | number | symbol]: any;
+    [K: string | number | symbol]: { [key: string]: string | number | boolean | Date | undefined | BaseConfig };
 };
 
 type ConfigContext<T extends BaseConfig> = {
@@ -41,7 +41,7 @@ interface Props {
 const ConfigProvider: React.FC<Props> = ({ children }) => {
     const [config, setConfig] = useState<BaseConfig>(getConfigFromLocalStorage());
 
-    const updateConfig = (key, value?: any, timeout?: Date) => {
+    const updateConfig = (key, value?, timeout?: Date) => {
         if (!key) throw new Error('Valid config key must be provided');
         if (value == undefined) {
             const newConfig = { ...config };
diff --git a/compendium-frontend/src/providers/NrenProvider.tsx b/compendium-frontend/src/providers/NrenProvider.tsx
index 1d8634b9..a7ac95d3 100644
--- a/compendium-frontend/src/providers/NrenProvider.tsx
+++ b/compendium-frontend/src/providers/NrenProvider.tsx
@@ -10,7 +10,7 @@ async function fetchNrens(): Promise<Nren[]> {
         const response = await fetch('/api/nren/list');
         const userList = await response.json();
         return userList
-    } catch (error) {
+    } catch {
         return [];
     }
 }
diff --git a/compendium-frontend/src/survey/Landing.tsx b/compendium-frontend/src/survey/Landing.tsx
index 0bf3f3f1..5e721e70 100644
--- a/compendium-frontend/src/survey/Landing.tsx
+++ b/compendium-frontend/src/survey/Landing.tsx
@@ -70,7 +70,7 @@ function Landing(): ReactElement {
         }
     }
 
-    function convertToExcel(jsonData: { name: string, data: any, meta: any }[]): Blob {
+    function convertToExcel(jsonData: { name: string, data, meta }[]): Blob {
         const wb = XLSX.utils.book_new();
         jsonData.forEach(sheet => {
             const ws = XLSX.utils.json_to_sheet(sheet.data);
diff --git a/compendium-frontend/src/survey/ShowUser.tsx b/compendium-frontend/src/survey/ShowUser.tsx
index 83d5d93b..f3aa6696 100644
--- a/compendium-frontend/src/survey/ShowUser.tsx
+++ b/compendium-frontend/src/survey/ShowUser.tsx
@@ -11,7 +11,7 @@ async function fetchUser(): Promise<User> {
         const response = await fetch('/api/user/');
         const user = await response.json();
         return user
-    } catch (error) {
+    } catch {
         return {
             'name': 'Error Fetching User'
         }
diff --git a/compendium-frontend/src/survey/SurveyContainerComponent.tsx b/compendium-frontend/src/survey/SurveyContainerComponent.tsx
index c516fc30..3e724a58 100644
--- a/compendium-frontend/src/survey/SurveyContainerComponent.tsx
+++ b/compendium-frontend/src/survey/SurveyContainerComponent.tsx
@@ -17,13 +17,13 @@ import { userContext } from "../providers/UserProvider";
 
 interface ValidationQuestion {
     name?: string;
-    value?: any;
+    value?: string | number | null;
     data?: ValidationQuestion;
 }
 
 // Overrides for questions that need to be validated differently from the default expression in their group
 const questionOverrides = {
-    data_protection_contact: (...args) => true, // don't validate the contact field, anything goes..
+    data_protection_contact: (..._args) => true, // don't validate the contact field, anything goes..
 }
 
 function oldValidateWebsiteUrl(params) {
@@ -43,12 +43,12 @@ function oldValidateWebsiteUrl(params) {
 
         const url = new URL(value);
         return !!url
-    } catch (err) {
+    } catch {
         return false;
     }
 }
 
-function validateQuestion(this: { question: ValidationQuestion, row?: any }, params: any) {
+function validateQuestion(this: { question: ValidationQuestion, row? }, params) {
     try {
         const question = this.question;
         const validator = params[0] || undefined;
@@ -111,13 +111,13 @@ function SurveyContainerComponent({ loadFrom }) {
 
     const pageHideListener = useCallback(() => {
         window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren);
-    }, []);
+    }, []); // eslint-disable-line react-hooks/exhaustive-deps
 
     const onPageExitThroughRouter = useCallback(() => {
         window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren);
         removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
         removeEventListener("pagehide", pageHideListener);
-    }, []);
+    }, []); // eslint-disable-line react-hooks/exhaustive-deps
 
     useEffect(() => {
         async function getModel() {
@@ -160,7 +160,7 @@ function SurveyContainerComponent({ loadFrom }) {
         getModel().catch(error => setError('Error when loading survey: ' + error.message)).then(() => {
             trackPageView({ documentTitle: `Survey for ${nren} (${year})` });
         })
-    }, []);
+    }, []); // eslint-disable-line react-hooks/exhaustive-deps
 
     if (!surveyModel) {
         return error;
diff --git a/compendium-frontend/src/survey/SurveyNavigationComponent.tsx b/compendium-frontend/src/survey/SurveyNavigationComponent.tsx
index 14a9753a..0d81f813 100644
--- a/compendium-frontend/src/survey/SurveyNavigationComponent.tsx
+++ b/compendium-frontend/src/survey/SurveyNavigationComponent.tsx
@@ -2,7 +2,6 @@ import React, { useContext, useEffect, useState, useCallback } from "react";
 import ProgressBar from './ProgressBar';
 import { Container, Row } from "react-bootstrap";
 import { userContext } from "../providers/UserProvider";
-import { ResponseStatus } from "./Schema";
 
 
 function SurveyNavigationComponent({ surveyModel, surveyActions, year, nren, children }) {
diff --git a/compendium-frontend/src/survey/api/survey.ts b/compendium-frontend/src/survey/api/survey.ts
index 48ba53af..44814d46 100644
--- a/compendium-frontend/src/survey/api/survey.ts
+++ b/compendium-frontend/src/survey/api/survey.ts
@@ -5,7 +5,7 @@ export async function fetchSurveys(): Promise<Survey[]> {
         const response = await fetch('/api/survey/list');
         const userList = await response.json();
         return userList
-    } catch (error) {
+    } catch {
         return [];
     }
 }
diff --git a/compendium-frontend/src/survey/management/UserManagementComponent.tsx b/compendium-frontend/src/survey/management/UserManagementComponent.tsx
index 9a911b5b..60c59d36 100644
--- a/compendium-frontend/src/survey/management/UserManagementComponent.tsx
+++ b/compendium-frontend/src/survey/management/UserManagementComponent.tsx
@@ -13,7 +13,7 @@ async function fetchUsers(): Promise<User[]> {
     try {
         const response = await fetch("/api/user/list");
         return await response.json()
-    } catch (error) {
+    } catch {
         return [];
     }
 }
@@ -22,7 +22,7 @@ async function fetchNrens(): Promise<Nren[]> {
     try {
         const response = await fetch("/api/nren/list");
         return await response.json()
-    } catch (error) {
+    } catch {
         return [];
     }
 }
diff --git a/compendium-frontend/src/survey/validation/validation.ts b/compendium-frontend/src/survey/validation/validation.ts
index a3cba3b8..59c60e56 100644
--- a/compendium-frontend/src/survey/validation/validation.ts
+++ b/compendium-frontend/src/survey/validation/validation.ts
@@ -14,7 +14,7 @@ function validateWebsiteUrl(value, nonEmpty = false) {
 
         const url = new URL(value);
         return !!url
-    } catch (err) {
+    } catch {
         return false;
     }
 }
-- 
GitLab