diff --git a/site/gatsby-site/cypress/e2e/integration/entities.cy.js b/site/gatsby-site/cypress/e2e/integration/entities.cy.js index 9e88f3f873..4db7eecfaf 100644 --- a/site/gatsby-site/cypress/e2e/integration/entities.cy.js +++ b/site/gatsby-site/cypress/e2e/integration/entities.cy.js @@ -67,4 +67,23 @@ describe('Entities page', () => { cy.visit(url); cy.get('[data-cy="row"]').first().contains('a', 'Facebook').should('be.visible'); }); + + conditionalIt( + !Cypress.env('isEmptyEnvironment') && Cypress.env('e2eUsername') && Cypress.env('e2ePassword'), + 'Should display Edit button only for Admin users', + () => { + cy.visit(url); + cy.get('[data-cy="edit-entity-btn"]').should('not.exist'); + + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + cy.visit(url); + cy.get('[data-cy="edit-entity-btn"]') + .first() + .should('have.attr', 'href', '/entities/edit?entity_id=facebook'); + cy.get('[data-cy="edit-entity-btn"]').first().click(); + cy.waitForStableDOM(); + cy.location('pathname').should('eq', '/entities/edit/'); + cy.location('search').should('eq', '?entity_id=facebook'); + } + ); }); diff --git a/site/gatsby-site/cypress/e2e/integration/entity.cy.js b/site/gatsby-site/cypress/e2e/integration/entity.cy.js index 24b7e5e576..684b7e648c 100644 --- a/site/gatsby-site/cypress/e2e/integration/entity.cy.js +++ b/site/gatsby-site/cypress/e2e/integration/entity.cy.js @@ -1,4 +1,4 @@ -import { maybeIt } from '../../support/utils'; +import { conditionalIt, maybeIt } from '../../support/utils'; import emptySubscriptionsData from '../../fixtures/subscriptions/empty-subscriptions.json'; import subscriptionsData from '../../fixtures/subscriptions/subscriptions.json'; const { SUBSCRIPTION_TYPE } = require('../../../src/utils/subscriptions'); @@ -118,4 +118,25 @@ describe('Individual Entity page', () => { .contains(`Please log in to subscribe`) .should('exist'); }); + + conditionalIt( + !Cypress.env('isEmptyEnvironment') && Cypress.env('e2eUsername') && Cypress.env('e2ePassword'), + 'Should display Edit button only for Admin users', + () => { + cy.visit(url); + cy.get('[data-cy="edit-entity-btn"]').should('not.exist'); + + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + cy.visit(url); + cy.get('[data-cy="edit-entity-btn"]').should( + 'have.attr', + 'href', + `/entities/edit?entity_id=${entity.entity_id}` + ); + cy.get('[data-cy="edit-entity-btn"]').click(); + cy.waitForStableDOM(); + cy.location('pathname').should('eq', '/entities/edit/'); + cy.location('search').should('eq', `?entity_id=${entity.entity_id}`); + } + ); }); diff --git a/site/gatsby-site/cypress/e2e/integration/entityEdit.cy.js b/site/gatsby-site/cypress/e2e/integration/entityEdit.cy.js new file mode 100644 index 0000000000..08ae036394 --- /dev/null +++ b/site/gatsby-site/cypress/e2e/integration/entityEdit.cy.js @@ -0,0 +1,122 @@ +import { conditionalIt } from '../../support/utils'; +import entity from '../../fixtures/entities/entity.json'; +import updateOneEntity from '../../fixtures/entities/updateOneEntity.json'; + +describe('Edit Entity', () => { + const entity_id = 'google'; + + const url = `/entities/edit?entity_id=${entity_id}`; + + conditionalIt( + !Cypress.env('isEmptyEnvironment') && Cypress.env('e2eUsername') && Cypress.env('e2ePassword'), + 'Should successfully edit Entity fields', + () => { + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + + cy.visit(url); + + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindEntity', + 'FindEntity', + entity + ); + + cy.wait(['@FindEntity']); + + const values = { + name: 'Google new', + }; + + Object.keys(values).forEach((key) => { + cy.get(`[name=${key}]`).clear().type(values[key]); + }); + + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'UpdateEntity', + 'UpdateEntity', + updateOneEntity + ); + + const now = new Date(); + + cy.clock(now); + + cy.contains('button', 'Save').click(); + + const updatedEntity = { + name: values.name, + date_modified: now.toISOString(), + }; + + cy.wait('@UpdateEntity').then((xhr) => { + expect(xhr.request.body.operationName).to.eq('UpdateEntity'); + expect(xhr.request.body.variables.query.entity_id).to.eq(entity_id); + expect(xhr.request.body.variables.set).to.deep.eq(updatedEntity); + }); + + cy.get('.tw-toast').contains('Entity updated successfully.').should('exist'); + } + ); + + conditionalIt( + !Cypress.env('isEmptyEnvironment') && Cypress.env('e2eUsername') && Cypress.env('e2ePassword'), + 'Should display an error message when editing Entity fails', + () => { + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + + cy.visit(url); + + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindEntity', + 'FindEntity', + entity + ); + + cy.wait(['@FindEntity']); + + const values = { + name: 'Google new', + }; + + Object.keys(values).forEach((key) => { + cy.get(`[name=${key}]`).clear().type(values[key]); + }); + + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'UpdateEntity', + 'UpdateEntity', + { + data: null, + errors: [ + { + message: 'Dummy error message', + }, + ], + } + ); + + const now = new Date(); + + cy.clock(now); + + cy.contains('button', 'Save').click(); + + const updatedEntity = { + name: values.name, + date_modified: now.toISOString(), + }; + + cy.wait('@UpdateEntity').then((xhr) => { + expect(xhr.request.body.operationName).to.eq('UpdateEntity'); + expect(xhr.request.body.variables.query.entity_id).to.eq(entity_id); + expect(xhr.request.body.variables.set).to.deep.eq(updatedEntity); + }); + + cy.get('.tw-toast').contains('Error updating Entity.').should('exist'); + } + ); +}); diff --git a/site/gatsby-site/cypress/fixtures/entities/entity.json b/site/gatsby-site/cypress/fixtures/entities/entity.json new file mode 100644 index 0000000000..bf937d338e --- /dev/null +++ b/site/gatsby-site/cypress/fixtures/entities/entity.json @@ -0,0 +1,10 @@ +{ + "data": { + "entity": { + "entity_id": "google", + "name": "Google", + "created_at": "2024-03-01T21:52:39.877Z", + "date_modified": "2024-03-21T21:52:39.877Z" + } + } +} diff --git a/site/gatsby-site/cypress/fixtures/entities/updateOneEntity.json b/site/gatsby-site/cypress/fixtures/entities/updateOneEntity.json new file mode 100644 index 0000000000..be791e188b --- /dev/null +++ b/site/gatsby-site/cypress/fixtures/entities/updateOneEntity.json @@ -0,0 +1,7 @@ +{ + "data": { + "updateOneEntity": { + "entity_id": "google" + } + } +} diff --git a/site/gatsby-site/i18n/locales/es/entities.json b/site/gatsby-site/i18n/locales/es/entities.json index 6adaec0514..313232db10 100644 --- a/site/gatsby-site/i18n/locales/es/entities.json +++ b/site/gatsby-site/i18n/locales/es/entities.json @@ -23,5 +23,9 @@ "Alleged: <2>2> developed and deployed an AI system, which harmed <5>5>.": "Presunto: un sistema de IA desarrollado e implementado por <2>2>, perjudicó a <5>5>.", "Alleged: <1>1> developed an AI system deployed by <4>4>, which harmed <6>6>.": "Presunto: un sistema de IA desarrollado por <1>1> e implementado por <4>4>, perjudicó a <6>6>.", "Entities involved in AI Incidents": "^Entities involved in AI Incidents", - "{{count}} Incident responses": "{{count}} respuestas de incidentes" + "{{count}} Incident responses": "{{count}} respuestas de incidentes", + "Editing Entity": "Editando Entidad", + "Back to Entity: {{name}}": "Volver a Entidad: {{name}}", + "Entity updated successfully.": "Entidad actualizada con éxito.", + "Error updating Entity.": "Error al actualizar la Entidad." } diff --git a/site/gatsby-site/i18n/locales/es/translation.json b/site/gatsby-site/i18n/locales/es/translation.json index 1b39b79769..eaaf4fcbbc 100644 --- a/site/gatsby-site/i18n/locales/es/translation.json +++ b/site/gatsby-site/i18n/locales/es/translation.json @@ -297,5 +297,9 @@ "AI News Digest": "Resumen de noticias de IA", "Remove Duplicate": "Eliminar Duplicado", "View History": "Ver Historial", - "CSET Annotators Table": "Tabla de Anotadores de CSET" + "CSET Annotators Table": "Tabla de Anotadores de CSET", + "Name": "Nombre", + "Creation Date": "Fecha de Creación", + "Last modified": "Última modificación", + "Save": "Guardar" } diff --git a/site/gatsby-site/i18n/locales/fr/entities.json b/site/gatsby-site/i18n/locales/fr/entities.json index 51aed0f958..0ab08c7066 100644 --- a/site/gatsby-site/i18n/locales/fr/entities.json +++ b/site/gatsby-site/i18n/locales/fr/entities.json @@ -23,5 +23,9 @@ "Alleged: <2>2> developed and deployed an AI system, which harmed <5>5>.": "Présumé : Un système d'IA développé et mis en œuvre par <2>2>, endommagé <5>5>.", "Alleged: <1>1> developed an AI system deployed by <4>4>, which harmed <6>6>.": "Présumé : un système d'IA développé par <1>1> et mis en œuvre par <4>4>, endommagé <6>6>.", "Entities involved in AI Incidents": "Entités impliquées dans les incidents IA", - "{{count}} Incident responses": "{{count}} réponses d'incident" + "{{count}} Incident responses": "{{count}} réponses d'incident", + "Editing Entity": "Modification de l'entité", + "Back to Entity: {{name}}": "Retour à l'entité: {{name}}", + "Entity updated successfully.": "Entité mise à jour avec succès.", + "Error updating Entity.": "Erreur lors de la mise à jour de l'entité." } diff --git a/site/gatsby-site/i18n/locales/fr/translation.json b/site/gatsby-site/i18n/locales/fr/translation.json index e9082d1577..9f050b8dcc 100644 --- a/site/gatsby-site/i18n/locales/fr/translation.json +++ b/site/gatsby-site/i18n/locales/fr/translation.json @@ -285,5 +285,9 @@ "AI News Digest": "Résumé de l’Actualité sur l’IA", "Remove Duplicate": "Supprimer le doublon", "View History": "Voir l'historique", - "CSET Annotators Table": "Tableau des annotateurs CSET" + "CSET Annotators Table": "Tableau des annotateurs CSET", + "Name": "Nom", + "Creation Date": "Date de création", + "Last modified": "Dernière modification", + "Save": "Enregistrer" } diff --git a/site/gatsby-site/i18n/locales/ja/entities.json b/site/gatsby-site/i18n/locales/ja/entities.json index 22895202ef..4f3e80aba2 100644 --- a/site/gatsby-site/i18n/locales/ja/entities.json +++ b/site/gatsby-site/i18n/locales/ja/entities.json @@ -24,5 +24,9 @@ "Alleged: <2>2> developed and deployed an AI system, which harmed <5>5>.": "推定: <2>2>が開発し提供したAIシステムで、<5>5>に影響を与えた", "Alleged: <1>1> developed an AI system deployed by <4>4>, which harmed <6>6>.": "推定: <1>1>が開発し、<4>4>が提供したAIシステムで、<6>6>に影響を与えた", "Entities involved in AI Incidents": "AIインシデントに関係する組織", - "{{count}} Incident responses": "{{count}} インシデントレスポンス" + "{{count}} Incident responses": "{{count}} インシデントレスポンス", + "Editing Entity": "組織の編集", + "Back to Entity: {{name}}": "組織に戻る: {{name}}", + "Entity updated successfully.": "組織が正常に更新されました。", + "Error updating Entity.": "組織の更新中にエラーが発生しました。" } diff --git a/site/gatsby-site/i18n/locales/ja/translation.json b/site/gatsby-site/i18n/locales/ja/translation.json index 056bb75961..1c17f80f7f 100644 --- a/site/gatsby-site/i18n/locales/ja/translation.json +++ b/site/gatsby-site/i18n/locales/ja/translation.json @@ -287,5 +287,9 @@ "AI News Digest": "AIニュースダイジェスト", "Remove Duplicate": "重複を削除する", "View History": "履歴を表示", - "CSET Annotators Table": "CSETアノテーターテーブル" + "CSET Annotators Table": "CSETアノテーターテーブル", + "Name": "名前", + "Creation Date": "作成日", + "Last modified": "最終更新", + "Save": "保存" } diff --git a/site/gatsby-site/src/components/entities/EntitiesTable.js b/site/gatsby-site/src/components/entities/EntitiesTable.js index bed2ad3ff7..88a3e2980c 100644 --- a/site/gatsby-site/src/components/entities/EntitiesTable.js +++ b/site/gatsby-site/src/components/entities/EntitiesTable.js @@ -5,6 +5,9 @@ import { faPlusCircle, faMinusCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Trans, useTranslation } from 'react-i18next'; import Table, { DefaultColumnFilter, DefaultColumnHeader } from 'components/ui/Table'; +import { Button } from 'flowbite-react'; +import { useUserContext } from 'contexts/userContext'; +import useLocalizePath from 'components/i18n/useLocalizePath'; function IncidentsCell({ cell }) { const { row, column } = cell; @@ -160,6 +163,10 @@ const sortByCount = (rowA, rowB, id) => { export default function EntitiesTable({ data, className = '', ...props }) { const { t } = useTranslation(['entities']); + const { loading: loadingUser, isRole } = useUserContext(); + + const localizePath = useLocalizePath(); + const defaultColumn = React.useMemo( () => ({ className: 'w-[120px]', @@ -261,8 +268,28 @@ export default function EntitiesTable({ data, className = '', ...props }) { }, ]; + if (!loadingUser && isRole('admin')) { + columns.push({ + title: t('Actions'), + accessor: 'actions', + disableFilters: true, + disableSortBy: true, + className: 'min-w-[120px]', + Cell: ({ row: { values } }) => ( + + ), + }); + } + return columns; - }, []); + }, [loadingUser]); const table = useTable( { diff --git a/site/gatsby-site/src/graphql/entities.js b/site/gatsby-site/src/graphql/entities.js index 46fc9f7ffb..e9f604d8cc 100644 --- a/site/gatsby-site/src/graphql/entities.js +++ b/site/gatsby-site/src/graphql/entities.js @@ -17,3 +17,22 @@ export const FIND_ENTITIES = gql` } } `; + +export const FIND_ENTITY = gql` + query FindEntity($query: EntityQueryInput) { + entity(query: $query) { + entity_id + name + created_at + date_modified + } + } +`; + +export const UPDATE_ENTITY = gql` + mutation UpdateEntity($query: EntityQueryInput, $set: EntityUpdateInput!) { + updateOneEntity(query: $query, set: $set) { + entity_id + } + } +`; diff --git a/site/gatsby-site/src/pages/entities/edit.js b/site/gatsby-site/src/pages/entities/edit.js new file mode 100644 index 0000000000..8d275e88d5 --- /dev/null +++ b/site/gatsby-site/src/pages/entities/edit.js @@ -0,0 +1,181 @@ +import React, { useEffect, useState } from 'react'; +import TextInputGroup from '../../components/forms/TextInputGroup'; +import { StringParam, useQueryParam, withDefault } from 'use-query-params'; +import useToastContext, { SEVERITY } from '../../hooks/useToast'; +import { Button, Spinner } from 'flowbite-react'; +import { FIND_ENTITY, UPDATE_ENTITY } from '../../graphql/entities'; +import { useMutation, useQuery } from '@apollo/client/react/hooks'; +import { Form, Formik } from 'formik'; +import { useTranslation, Trans } from 'react-i18next'; +import { Link } from 'gatsby'; +import DefaultSkeleton from 'elements/Skeletons/Default'; +import * as Yup from 'yup'; +import { format } from 'date-fns'; + +const schema = Yup.object().shape({ + name: Yup.string().required(), +}); + +function EditEntityPage(props) { + const { t } = useTranslation(); + + const [entity, setEntity] = useState(null); + + const [entityId] = useQueryParam('entity_id', withDefault(StringParam, '')); + + const { + data: entityData, + loading: loadingEntity, + refetch, + } = useQuery(FIND_ENTITY, { + variables: { query: { entity_id: entityId } }, + }); + + const loading = loadingEntity; + + const [updateEntityMutation] = useMutation(UPDATE_ENTITY); + + const addToast = useToastContext(); + + useEffect(() => { + if (entityData?.entity) { + setEntity({ + ...entityData.entity, + }); + } + }, [entityData]); + + const handleSubmit = async (values) => { + try { + await updateEntityMutation({ + variables: { + query: { + entity_id: entityId, + }, + set: { + name: values.name, + date_modified: new Date(), + }, + }, + }); + + refetch(); + + addToast({ + message: t('Entity updated successfully.'), + severity: SEVERITY.success, + }); + } catch (error) { + addToast({ + message: t('Error updating Entity.'), + severity: SEVERITY.danger, + error, + }); + } + }; + + return ( +