diff --git a/package-lock.json b/package-lock.json index e323476..8738915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9665,14 +9665,14 @@ } }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/axobject-query": { @@ -23460,9 +23460,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pump": { "version": "3.0.0", diff --git a/src/components/data-source-item-editor/index.tsx b/src/components/data-source-item-editor/index.tsx index bf3e561..904881a 100644 --- a/src/components/data-source-item-editor/index.tsx +++ b/src/components/data-source-item-editor/index.tsx @@ -22,7 +22,7 @@ import withOrganizations from '../with-organizations'; import * as OrganizationActions from '../with-organizations/redux/actions'; -interface FormValues extends Omit {} +interface FormValues extends Omit {} interface ExternalProps { dataSource?: Partial; @@ -358,7 +358,11 @@ const formikConfig: WithFormikConfig = { }), handleSubmit: (values, { props: { onSave, dataSource } }) => onSave( - { id: dataSource?.id ?? '', ...values }, + { + id: dataSource?.id ?? '', + active: dataSource?.active ?? true, + ...values + }, dataSource?.publisherId ?? values.publisherId, !!dataSource ), diff --git a/src/components/data-source-item/index.tsx b/src/components/data-source-item/index.tsx index 8018500..f6a609c 100644 --- a/src/components/data-source-item/index.tsx +++ b/src/components/data-source-item/index.tsx @@ -1,9 +1,12 @@ -import React, { memo, FC } from 'react'; +import React, { memo, FC, useState } from 'react'; import { compose } from 'redux'; import { Variant } from '@fellesdatakatalog/button'; import Link from '@fellesdatakatalog/link'; import InfoIcon from '@material-ui/icons/Info'; +import PowerIcon from '@material-ui/icons/Power'; +import PowerOffIcon from '@material-ui/icons/PowerOff'; +import ConfirmDialog from '../confirm-dialog'; import SC from './styled'; import ConceptIcon from '../../images/concept-icon.svg'; @@ -30,6 +33,11 @@ interface Props { onDataSourceHarvestStatus: (id: string, organizationId: string) => void; onDataSourceItemEdit: (id: string, organizationId: string) => void; onDataSourceItemRemove: (id: string, organizationId: string) => void; + onDataSourceToggleActive: ( + id: string, + organizationId: string, + currentlyActive: boolean + ) => void; } const DataSourceItem: FC = ({ @@ -39,15 +47,19 @@ const DataSourceItem: FC = ({ url, acceptHeaderValue, description, - publisherId + publisherId, + active }, organization, harvestStates, onDataSourceItemHarvest, onDataSourceHarvestStatus, onDataSourceItemEdit, - onDataSourceItemRemove + onDataSourceItemRemove, + onDataSourceToggleActive }) => { + const isActive = active !== false; + const [showActivateConfirm, setShowActivateConfirm] = useState(false); const DataSourceType = () => { switch (dataType) { case DataType.DATASET: @@ -138,52 +150,87 @@ const DataSourceItem: FC = ({ } const DatasetItemControls = () => ( - - {statusLabel && ( - + + {statusLabel && isActive && ( + + {statusLabel} + + )} + {isActive && ( + onDataSourceItemHarvest(id, publisherId)} + $dataType={dataType} + > + + Høst + + )} + onDataSourceHarvestStatus(id, publisherId)} + variant={Variant.SECONDARY} $dataType={dataType} - $hasError={!!displayState?.errorMessage} > - {statusLabel} - + + Status + + onDataSourceItemEdit(id, publisherId)} + variant={Variant.SECONDARY} + $dataType={dataType} + > + + Rediger + + setShowActivateConfirm(true)} + $dataType={dataType} + > + {isActive ? ( + <> + + Deaktiver + + ) : ( + <> + + Aktiver + + )} + + onDataSourceItemRemove(id, publisherId)} + variant={Variant.TERTIARY} + $dataType={dataType} + > + + Slett + + + {showActivateConfirm && ( + { + setShowActivateConfirm(false); + onDataSourceToggleActive(id, publisherId, isActive); + }} + onCancel={() => setShowActivateConfirm(false)} + /> )} - onDataSourceItemHarvest(id, publisherId)} - $dataType={dataType} - > - - Høst - - onDataSourceHarvestStatus(id, publisherId)} - variant={Variant.SECONDARY} - $dataType={dataType} - > - - Status - - onDataSourceItemEdit(id, publisherId)} - variant={Variant.SECONDARY} - $dataType={dataType} - > - - Rediger - - - onDataSourceItemRemove(id, publisherId)} - variant={Variant.TERTIARY} - $dataType={dataType} - > - - Slett - - + ); return ( - +
diff --git a/src/components/data-source-item/styled.ts b/src/components/data-source-item/styled.ts index 33c61e7..bb05546 100644 --- a/src/components/data-source-item/styled.ts +++ b/src/components/data-source-item/styled.ts @@ -8,9 +8,11 @@ import { DataType } from '../../types/enums'; interface Props { $dataType?: DataType | null; $hasError?: boolean; + $inactive?: boolean; } -const DataSourceItem = styled.li` +const DataSourceItem = styled.li` + position: relative; display: flex; align-items: center; min-height: 170px; @@ -22,6 +24,19 @@ const DataSourceItem = styled.li` :nth-of-type(n + 2) { margin-top: 15px; } + + ${({ $inactive }) => + $inactive && + css` + &::after { + content: ''; + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.45); + border-radius: 5px; + pointer-events: none; + } + `} `; const DataSourceType = styled.div` @@ -516,6 +531,78 @@ const TertiaryButton = styled(ButtonBase)` } `; +const ActivateButton = styled(SecondaryButton)` + position: relative; + z-index: 1; + + ${({ $dataType }) => { + switch ($dataType) { + case DataType.CONCEPT: + return css` + background: #d5edf2; + color: #2e6773; + & > svg > path { + fill: #2e6773; + } + `; + case DataType.DATASERVICE: + return css` + background: #f2e1d5; + color: #805333; + & > svg > path { + fill: #805333; + } + `; + case DataType.DATASET: + return css` + background: #d5e1f2; + color: #335380; + & > svg > path { + fill: #335380; + } + `; + case DataType.INFORMATION_MODEL: + return css` + background: #e4d5f2; + color: #593380; + & > svg > path { + fill: #593380; + } + `; + case DataType.PUBLIC_SERVICE: + return css` + background: #f2d5e1; + color: #803353; + & > svg > path { + fill: #803353; + } + `; + default: + return css``; + } + }}; + + &:hover, + &:focus { + color: ${theme.colour(Colour.NEUTRAL, 'N0')}; + & > svg > path { + fill: ${theme.colour(Colour.NEUTRAL, 'N0')}; + } + } +`; + +const InactiveBadge = styled.span` + display: inline-flex; + align-items: center; + padding: 4px 10px; + border-radius: 4px; + font-size: ${theme.fontSize('FS14')}; + font-weight: ${theme.fontWeight('FW500')}; + margin-right: 10px; + background: #fff3e0; + color: #e65100; +`; + export default { DataSourceItem, DataSourceType, @@ -530,5 +617,7 @@ export default { HarvestButton, EditButton, ValidateLink, - TertiaryButton + TertiaryButton, + ActivateButton, + InactiveBadge }; diff --git a/src/components/data-sources-page/components/data-sources-page/index.tsx b/src/components/data-sources-page/components/data-sources-page/index.tsx index 54d1e41..e8d5cdf 100644 --- a/src/components/data-sources-page/components/data-sources-page/index.tsx +++ b/src/components/data-sources-page/components/data-sources-page/index.tsx @@ -85,7 +85,9 @@ const DataSourcesPage: FC = ({ removeDataSourceRequested, harvestDataSourceRequested, harvestStatusRequested, - clearSaveStatus + clearSaveStatus, + activateDataSourceRequested, + deactivateDataSourceRequested }, organizations, organizationActions: { fetchOrganizationsRequested }, @@ -191,12 +193,26 @@ const DataSourcesPage: FC = ({ }; const harvestDataSourceItem = (id: string, organizationId: string) => { + const ds = dataSources.find(s => s.id === id); + if (ds && ds.active === false) return; harvestDataSourceRequested(id, organizationId, { removeAll: false, forced: true }); }; + const toggleDataSourceActive = ( + id: string, + organizationId: string, + currentlyActive: boolean + ) => { + if (currentlyActive) { + deactivateDataSourceRequested(id, organizationId); + } else { + activateDataSourceRequested(id, organizationId); + } + }; + const showDataSourceItemHarvestStatus = ( id: string, organizationId: string @@ -408,6 +424,7 @@ const DataSourcesPage: FC = ({ onDataSourceHarvestStatus={showDataSourceItemHarvestStatus} onDataSourceItemEdit={showDataSourceItemEditor} onDataSourceItemRemove={showConfirm} + onDataSourceToggleActive={toggleDataSourceActive} /> ))} {!fetchingDataSources && filteredDataSources.length === 0 && ( @@ -449,6 +466,9 @@ const DataSourcesPage: FC = ({ ds.id === dataSourceId)?.description} dataSourceId={dataSourceId ?? undefined} + dataSourceIsActive={ + dataSources.find(ds => ds.id === dataSourceId)?.active + } harvestStates={harvestStatus} onDiscard={hideDataSourceItemHarvestStatus} /> diff --git a/src/components/harvest-status-modal/index.tsx b/src/components/harvest-status-modal/index.tsx index 9413d04..c345bef 100644 --- a/src/components/harvest-status-modal/index.tsx +++ b/src/components/harvest-status-modal/index.tsx @@ -43,6 +43,7 @@ const DATA_TYPE_LABELS: Record = { interface ExternalProps { name?: string; dataSourceId?: string; + dataSourceIsActive?: boolean; harvestStates?: HarvestCurrentState[]; onDiscard: () => void; } @@ -77,6 +78,7 @@ const formatLocalDateTime = (value?: string) => { const HarvestStatusModal: FC = ({ name, dataSourceId, + dataSourceIsActive, harvestStates, onDiscard }) => { @@ -87,8 +89,14 @@ const HarvestStatusModal: FC = ({ {name} - - Oppdaterer status hvert 5. sekund … + {dataSourceIsActive ? ( + <> + + Oppdaterer status hvert 5. sekund … + + ) : ( + Høsting er deaktivert + )} {hasHarvestStates && harvestStates.map(state => { diff --git a/src/components/with-data-sources/redux/action-types.ts b/src/components/with-data-sources/redux/action-types.ts index 00db97a..372250c 100644 --- a/src/components/with-data-sources/redux/action-types.ts +++ b/src/components/with-data-sources/redux/action-types.ts @@ -38,3 +38,17 @@ export const FETCH_DATASOURCE_STATUS_SUCCEEDED = 'FETCH_DATASOURCE_STATUS_SUCCEEDED' as const; export const FETCH_DATASOURCE_STATUS_FAILED = 'FETCH_DATASOURCE_STATUS_FAILED' as const; + +export const ACTIVATE_DATA_SOURCE_REQUESTED = + 'ACTIVATE_DATA_SOURCE_REQUESTED' as const; +export const ACTIVATE_DATA_SOURCE_SUCCEEDED = + 'ACTIVATE_DATA_SOURCE_SUCCEEDED' as const; +export const ACTIVATE_DATA_SOURCE_FAILED = + 'ACTIVATE_DATA_SOURCE_FAILED' as const; + +export const DEACTIVATE_DATA_SOURCE_REQUESTED = + 'DEACTIVATE_DATA_SOURCE_REQUESTED' as const; +export const DEACTIVATE_DATA_SOURCE_SUCCEEDED = + 'DEACTIVATE_DATA_SOURCE_SUCCEEDED' as const; +export const DEACTIVATE_DATA_SOURCE_FAILED = + 'DEACTIVATE_DATA_SOURCE_FAILED' as const; diff --git a/src/components/with-data-sources/redux/actions.ts b/src/components/with-data-sources/redux/actions.ts index 8b3193a..31a5b84 100644 --- a/src/components/with-data-sources/redux/actions.ts +++ b/src/components/with-data-sources/redux/actions.ts @@ -19,7 +19,13 @@ import { UPDATE_DATA_SOURCE_FAILED, UPDATE_DATA_SOURCE_REQUESTED, UPDATE_DATA_SOURCE_SUCCEEDED, - CLEAR_SAVE_STATUS + CLEAR_SAVE_STATUS, + ACTIVATE_DATA_SOURCE_REQUESTED, + ACTIVATE_DATA_SOURCE_SUCCEEDED, + ACTIVATE_DATA_SOURCE_FAILED, + DEACTIVATE_DATA_SOURCE_REQUESTED, + DEACTIVATE_DATA_SOURCE_SUCCEEDED, + DEACTIVATE_DATA_SOURCE_FAILED } from './action-types'; import type { DataSource, HarvestCurrentState } from '../../../types'; @@ -223,3 +229,45 @@ export function removeDataSourceFailed(message: string) { } }; } + +export function activateDataSourceRequested(id: string, org: string) { + return { + type: ACTIVATE_DATA_SOURCE_REQUESTED, + payload: { id, org } + }; +} + +export function activateDataSourceSucceeded(dataSource: DataSource) { + return { + type: ACTIVATE_DATA_SOURCE_SUCCEEDED, + payload: { dataSource } + }; +} + +export function activateDataSourceFailed(message: string) { + return { + type: ACTIVATE_DATA_SOURCE_FAILED, + payload: { message } + }; +} + +export function deactivateDataSourceRequested(id: string, org: string) { + return { + type: DEACTIVATE_DATA_SOURCE_REQUESTED, + payload: { id, org } + }; +} + +export function deactivateDataSourceSucceeded(dataSource: DataSource) { + return { + type: DEACTIVATE_DATA_SOURCE_SUCCEEDED, + payload: { dataSource } + }; +} + +export function deactivateDataSourceFailed(message: string) { + return { + type: DEACTIVATE_DATA_SOURCE_FAILED, + payload: { message } + }; +} diff --git a/src/components/with-data-sources/redux/reducer.ts b/src/components/with-data-sources/redux/reducer.ts index aaff2a2..f508bd6 100644 --- a/src/components/with-data-sources/redux/reducer.ts +++ b/src/components/with-data-sources/redux/reducer.ts @@ -18,7 +18,9 @@ import { HARVEST_STATUS_REQUESTED, HARVEST_STATUS_SUCCEEDED, HARVEST_STATUS_FAILED, - CLEAR_SAVE_STATUS + CLEAR_SAVE_STATUS, + ACTIVATE_DATA_SOURCE_SUCCEEDED, + DEACTIVATE_DATA_SOURCE_SUCCEEDED } from './action-types'; import { Actions } from '../../../types'; @@ -109,6 +111,15 @@ export default function reducer( ); case HARVEST_STATUS_FAILED: return state.set('harvestStatus', undefined); + case ACTIVATE_DATA_SOURCE_SUCCEEDED: + case DEACTIVATE_DATA_SOURCE_SUCCEEDED: + return state.update('dataSources', (dataSources: any) => + dataSources.map((dataSource: any) => + dataSource.get('id') === action.payload.dataSource.id + ? fromJS(action.payload.dataSource) + : dataSource + ) + ); default: return state; } diff --git a/src/components/with-data-sources/redux/saga.ts b/src/components/with-data-sources/redux/saga.ts index 45f9579..168534b 100644 --- a/src/components/with-data-sources/redux/saga.ts +++ b/src/components/with-data-sources/redux/saga.ts @@ -10,7 +10,9 @@ import { HARVEST_STATUS_REQUESTED, REGISTER_DATA_SOURCE_REQUESTED, UPDATE_DATA_SOURCE_REQUESTED, - REMOVE_DATA_SOURCE_REQUESTED + REMOVE_DATA_SOURCE_REQUESTED, + ACTIVATE_DATA_SOURCE_REQUESTED, + DEACTIVATE_DATA_SOURCE_REQUESTED } from './action-types'; import { DataSource, HarvestCurrentState } from '../../../types'; @@ -228,6 +230,54 @@ function* harvestStatusRequested({ } } +function* activateDataSourceRequested({ + payload: { id, org } +}: ReturnType) { + try { + const auth = yield getContext('auth'); + const authorization = yield call([auth, auth.getAuthorizationHeader]); + const { data, status } = yield call( + axios.post, + `${FDK_HARVEST_ADMIN_HOST}/organizations/${org}/datasources/${id}/activate`, + undefined, + { headers: { authorization } } + ); + if (status === 200) { + yield put(actions.activateDataSourceSucceeded(data as DataSource)); + } else { + yield put( + actions.activateDataSourceFailed('Kunne ikke aktivere datakilde.') + ); + } + } catch (e: any) { + yield put(actions.activateDataSourceFailed(e.message)); + } +} + +function* deactivateDataSourceRequested({ + payload: { id, org } +}: ReturnType) { + try { + const auth = yield getContext('auth'); + const authorization = yield call([auth, auth.getAuthorizationHeader]); + const { data, status } = yield call( + axios.post, + `${FDK_HARVEST_ADMIN_HOST}/organizations/${org}/datasources/${id}/deactivate`, + undefined, + { headers: { authorization } } + ); + if (status === 200) { + yield put(actions.deactivateDataSourceSucceeded(data as DataSource)); + } else { + yield put( + actions.deactivateDataSourceFailed('Kunne ikke deaktivere datakilde.') + ); + } + } catch (e: any) { + yield put(actions.deactivateDataSourceFailed(e.message)); + } +} + export default function* saga() { yield all([ takeLatest(FETCH_DATA_SOURCES_REQUESTED, fetchDataSourcesRequested), @@ -235,6 +285,8 @@ export default function* saga() { takeLatest(HARVEST_STATUS_REQUESTED, harvestStatusRequested), takeLatest(REGISTER_DATA_SOURCE_REQUESTED, registerDataSourceRequested), takeLatest(UPDATE_DATA_SOURCE_REQUESTED, updateDataSourceRequested), - takeLatest(REMOVE_DATA_SOURCE_REQUESTED, removeDataSourceRequested) + takeLatest(REMOVE_DATA_SOURCE_REQUESTED, removeDataSourceRequested), + takeLatest(ACTIVATE_DATA_SOURCE_REQUESTED, activateDataSourceRequested), + takeLatest(DEACTIVATE_DATA_SOURCE_REQUESTED, deactivateDataSourceRequested) ]); } diff --git a/src/types/domain.d.ts b/src/types/domain.d.ts index 8beeea7..3105b6f 100644 --- a/src/types/domain.d.ts +++ b/src/types/domain.d.ts @@ -9,6 +9,7 @@ export interface DataSource { description: string; acceptHeaderValue: MimeType | null; authHeader: AuthHeader | null; + active: boolean; } export interface Delegatee {