diff --git a/tornjak-frontend/package-lock.json b/tornjak-frontend/package-lock.json index 0b2ba22d..4f2ef5b5 100644 --- a/tornjak-frontend/package-lock.json +++ b/tornjak-frontend/package-lock.json @@ -40,6 +40,7 @@ "file-saver": "^2.0.5", "jwt-decode": "^3.1.2", "keycloak-js": "^19.0.1", + "moment": "^2.29.4", "node-sass": "^6.0.1", "prop-types": "^15.6.0", "puppeteer": "^10.4.0", @@ -15211,6 +15212,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/moo": { "version": "0.5.2", "dev": true, @@ -35110,6 +35119,11 @@ "minimist": "^1.2.6" } }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "moo": { "version": "0.5.2", "dev": true diff --git a/tornjak-frontend/package.json b/tornjak-frontend/package.json index ceb954fa..022c8923 100644 --- a/tornjak-frontend/package.json +++ b/tornjak-frontend/package.json @@ -35,6 +35,7 @@ "file-saver": "^2.0.5", "jwt-decode": "^3.1.2", "keycloak-js": "^19.0.1", + "moment": "^2.29.4", "node-sass": "^6.0.1", "prop-types": "^15.6.0", "puppeteer": "^10.4.0", diff --git a/tornjak-frontend/src/components/entry-create.tsx b/tornjak-frontend/src/components/entry-create.tsx index 9e15cc4a..084f35c9 100644 --- a/tornjak-frontend/src/components/entry-create.tsx +++ b/tornjak-frontend/src/components/entry-create.tsx @@ -19,7 +19,7 @@ import { tornjakServerInfoUpdateFunc, serverInfoUpdateFunc, agentworkloadSelectorInfoFunc, - newEntriesUpdateFunc + newEntriesUpdateFunc, } from 'redux/actions'; import { EntriesList, @@ -32,10 +32,13 @@ import { WorkloadSelectorInfoLabels, } from './types'; import { RootState } from 'redux/reducers'; -import CreateEntryYaml from './entry-create-json'; +import EntryExpiryFeatures from './entry-expiry-features'; +import CreateEntryJson from './entry-create-json'; // import PropTypes from "prop-types"; // needed for testing will be removed on last pr type CreateEntryProp = { + // entry expiry time + globalEntryExpiryTime: number, // dispatches a payload for the server selected and has a return type of void serverSelectedFunc: (globalServerSelected: string) => void, // dispatches a payload for list of agents with their metadata info as an array of AgentListType and has a return type of void @@ -610,7 +613,7 @@ class CreateEntry extends Component { "selectors": selectorEntries, "admin": this.state.adminFlag, "ttl": this.state.ttl, - "expires_at": this.state.expiresAt, + "expires_at": this.props.globalEntryExpiryTime, "downstream": this.state.downstream, "federates_with": federatedWithList, "dns_names": dnsNamesWithList, @@ -724,7 +727,7 @@ class CreateEntry extends Component { Upload New Entry/ Entries} open>
-

{this.props.globalNewEntries.length === 0 && @@ -855,17 +858,7 @@ class CreateEntry extends Component { />
- +
({ globalSelectorInfo: state.servers.globalSelectorInfo, globalAgentsList: state.agents.globalAgentsList, globalEntriesList: state.entries.globalEntriesList, + globalEntryExpiryTime: state.entries.globalEntryExpiryTime, globalNewEntries: state.entries.globalNewEntries, globalServerInfo: state.servers.globalServerInfo, globalTornjakServerInfo: state.servers.globalTornjakServerInfo, diff --git a/tornjak-frontend/src/components/entry-expiry-features.tsx b/tornjak-frontend/src/components/entry-expiry-features.tsx new file mode 100644 index 00000000..9b09a93e --- /dev/null +++ b/tornjak-frontend/src/components/entry-expiry-features.tsx @@ -0,0 +1,231 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Dropdown, TextInput, NumberInput, InlineNotification } from 'carbon-components-react'; +import TornjakApi from './tornjak-api-helpers'; +import './style.css'; +import SpiffeHelper from './spiffe-helper'; +import { + entryExpiryUpdateFunc +} from 'redux/actions'; +import { RootState } from 'redux/reducers'; +var moment = require('moment'); +const JSMaxSafeTime = 8640000000000 // In seconds - JS cannot represent times safely larger than this + +type EntryExpiryProp = { + // dispatches a payload for the entry expiry time and has a return type of void + entryExpiryUpdateFunc: (globalEntryExpiryTime: number) => void, +} + +type EntryExpiryState = { + expiresAt: number, + expiryOption: string, + expiryOptionList: String[], + expiryDate: string, + expiryTime: string, + expiryUnsafe: boolean, + expiryInvalid: boolean, +} + +class EntryExpiryFeatures extends Component { + TornjakApi: TornjakApi; + SpiffeHelper: SpiffeHelper; + constructor(props: EntryExpiryProp) { + super(props); + this.TornjakApi = new TornjakApi(props); + this.SpiffeHelper = new SpiffeHelper(props); + this.onChangeExpiryOption = this.onChangeExpiryOption.bind(this); + this.expiryTimeUpdate = this.expiryTimeUpdate.bind(this); + this.onChangeExpiresAtSeconds = this.onChangeExpiresAtSeconds.bind(this); + this.isValidDate = this.isValidDate.bind(this); + this.updateValidDateAndTime = this.updateValidDateAndTime.bind(this); + this.onChangeExpiresAtTime = this.onChangeExpiresAtTime.bind(this); + this.onChangeExpiresAtDate = this.onChangeExpiresAtDate.bind(this); + this.state = { + expiryOption: "None", + expiryOptionList: ["None", "Seconds Since Epoch", "Date/Time"], + expiryDate: "1/1/2021", + expiryTime: "00:00", + expiresAt: 0, + expiryUnsafe: false, + expiryInvalid: false, + } + } + + componentDidMount() { + } + + componentDidUpdate(prevProps: EntryExpiryProp, prevState: EntryExpiryState) { + this.props.entryExpiryUpdateFunc(this.state.expiresAt); + } + + onChangeExpiryOption(e: { selectedItem: any; }) { + this.setState({ + expiresAt: 0, + expiryOption: e.selectedItem, + expiryUnsafe: false, + expiryInvalid: false + }); + } + + isValidExpiryTime(seconds: number) { + return seconds > 0 && seconds <= JSMaxSafeTime + } + + expiryTimeUpdate(seconds: number) { + this.setState({ + expiresAt: seconds, + expiryUnsafe: !this.isValidExpiryTime(seconds) + }) + } + + onChangeExpiresAtSeconds(e: any) { + var seconds = Number(e.imaginaryTarget.value) + this.expiryTimeUpdate(seconds) + } + + isValidDate(d: Date) { // date is successfully translated in Javascript + return d instanceof Date && !isNaN(d.getTime()); + } + + updateValidDateAndTime(d: string, t: string) { + var mo = moment(d + ' ' + t, "MM/DD/YYYY hh:mm:ss", true) + var testDate = mo._d; //extract date + this.setState({ // should always reflect what the user sees + expiryDate: d, + expiryTime: t + }) + if (this.isValidDate(testDate) && mo.isValid()) { + this.setState({ + expiryInvalid: false, + }) + var mstoSecConvFactor = 1000; + var seconds = Math.floor(testDate.getTime() / mstoSecConvFactor) + this.expiryTimeUpdate(seconds) + console.log(d, t, this.state.expiryDate, this.state.expiryTime) + return + } + this.setState({ + expiryInvalid: true, + }) + } + + onChangeExpiresAtDate(e: { target: { value: string; }; }) { + this.updateValidDateAndTime(e.target.value, this.state.expiryTime) + } + + onChangeExpiresAtTime(e: { target: { value: string; }; }) { + this.updateValidDateAndTime(this.state.expiryDate, e.target.value) + } + + + render() { + return ( +
+
+ +
+ {this.state.expiryOption !== "None" &&
+ {this.state.expiryOption === "Seconds Since Epoch" && +
+
+ +
+
+ } + {this.state.expiryOption === "Date/Time" && +
+
+ +
+
+ +
+
+ } +
+ } + {(this.state.expiryUnsafe || this.state.expiryInvalid) && +
+ Expiry time either in invalid format/ negative/ too large. Submitting this time may result in undefined behavior. } + > + + {this.state.expiryOption === "Seconds Since Epoch" && this.state.expiryUnsafe && + Seconds must be positive and less than MaxSafeTime="{JSMaxSafeTime} Seconds" } + > + + } + {this.state.expiryOption === "Date/Time" && this.state.expiryUnsafe && + Date must be past January 1, 1970 @ 12:00AM GMT" } + > + + } + {this.state.expiryOption === "Date/Time" && this.state.expiryInvalid && + Date or time format is invalid } + > + + } +
+ } +
+ ) + } +} + + +const mapStateToProps = (state: RootState) => ({ +}) + +export default connect( + mapStateToProps, + { entryExpiryUpdateFunc } +)(EntryExpiryFeatures) + +export { EntryExpiryFeatures }; \ No newline at end of file diff --git a/tornjak-frontend/src/components/style.css b/tornjak-frontend/src/components/style.css index d54d4571..1216a99f 100644 --- a/tornjak-frontend/src/components/style.css +++ b/tornjak-frontend/src/components/style.css @@ -175,9 +175,35 @@ } .expiresAt-input { - display: inline-block; + margin-right: 20px; margin-bottom: 20px; - width: 160px; + width: 600px; + } + + .expiry-drop-down { + display: inline-block; + vertical-align: top; + margin-right: 20px; + width: 200px; + } + + .expiryEntryFields { + display: inline-block; + vertical-align: top; + width: 200px; + } + + .expiryOption-field { + display: inline-block; + vertical-align: top; + width: 500px; + } + + .expiryOption-entry { + margin-right: 20px; + display: inline-block; + vertical-align: top; + width: 200px; } .federates-with-input-field { diff --git a/tornjak-frontend/src/redux/actions/index.ts b/tornjak-frontend/src/redux/actions/index.ts index 92ffbdb4..2c8b6f7e 100644 --- a/tornjak-frontend/src/redux/actions/index.ts +++ b/tornjak-frontend/src/redux/actions/index.ts @@ -35,6 +35,8 @@ import { ClickedDashboardTableAction, NewEntriesAction, GLOBAL_NEW_ENTRIES, + EntryExpiryAction, + GLOBAL_ENTRY_EXPIRY, } from './types'; import { @@ -239,6 +241,17 @@ export function newEntriesUpdateFunc(globalNewEntries: EntriesList[]): ThunkActi } } +// Expected input - entry expiry time +// entryExpiryUpdateFunc returns the entry expiry time if set +export function entryExpiryUpdateFunc(globalEntryExpiryTime: number): ThunkAction { + return dispatch => { + dispatch({ + type: GLOBAL_ENTRY_EXPIRY, + payload: globalEntryExpiryTime + }); + } +} + // Expected input - List of agents with their info // json representation from SPIFFE golang documentation - https://github.com/spiffe/spire/blob/v0.12.0/proto/spire/types/agent.pb.go#L28-L45 // agentsListUpdateFunc returns the list of agents with their info diff --git a/tornjak-frontend/src/redux/actions/types.ts b/tornjak-frontend/src/redux/actions/types.ts index f159f42b..0ddb7fc0 100644 --- a/tornjak-frontend/src/redux/actions/types.ts +++ b/tornjak-frontend/src/redux/actions/types.ts @@ -66,10 +66,12 @@ export interface ClusterTypeInfoAction extends Action { @@ -80,6 +82,10 @@ export interface NewEntriesAction extends Action { payload: EntriesList[]; } +export interface EntryExpiryAction extends Action { + payload: number; +} + // servers export const GLOBAL_SERVER_SELECTED = 'GLOBAL_SERVER_SELECTED'; export const GLOBAL_SERVER_INFO = 'GLOBAL_SERVER_INFO'; diff --git a/tornjak-frontend/src/redux/reducers/entriesReducer.ts b/tornjak-frontend/src/redux/reducers/entriesReducer.ts index 26796ac5..eff0cebc 100644 --- a/tornjak-frontend/src/redux/reducers/entriesReducer.ts +++ b/tornjak-frontend/src/redux/reducers/entriesReducer.ts @@ -1,17 +1,20 @@ import { EntriesListAction, NewEntriesAction, + EntryExpiryAction, EntriesReducerState, GLOBAL_ENTRIES_LIST, GLOBAL_NEW_ENTRIES, + GLOBAL_ENTRY_EXPIRY } from '../actions/types'; const initialState: EntriesReducerState = { globalEntriesList: [], - globalNewEntries: [] + globalNewEntries: [], + globalEntryExpiryTime: 0, }; -export default function entriesReducer(state: EntriesReducerState = initialState, action: EntriesListAction | NewEntriesAction) { +export default function entriesReducer(state: EntriesReducerState = initialState, action: EntriesListAction | NewEntriesAction | EntryExpiryAction) { switch (action.type) { case GLOBAL_ENTRIES_LIST: return { @@ -23,6 +26,11 @@ export default function entriesReducer(state: EntriesReducerState = initialState ...state, globalNewEntries: action.payload }; + case GLOBAL_ENTRY_EXPIRY: + return { + ...state, + globalEntryExpiryTime: action.payload + }; default: return state; }