22 | } else if(images.length == 0){
23 | return
24 | }
25 | return
31 |
32 | { showModal && }
33 | {
34 | images.map((image, index) =>
35 |
40 | )
41 | }
42 |
43 | }
44 |
45 | }
46 |
47 | const mapStateToProps = state => {
48 | return {
49 | images: state.image.images,
50 | showModal: state.image.showModal,
51 | selectedImage: state.image.selectedImage,
52 | loading: state.image.loading
53 | }
54 | }
55 |
56 | const mapDispatchToProps = dispatch => bindActionCreators(
57 | {
58 | getImages,
59 | toggleImageDeleteModal
60 | },
61 | dispatch
62 | )
63 |
64 | export default connect(mapStateToProps, mapDispatchToProps)( ImageList )
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0 !important;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif !important;
6 | -webkit-font-smoothing: antialiased !important;
7 | -moz-osx-font-smoothing: grayscale !important;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace !important;
13 | }
14 |
15 | div.subnavaware-view {
16 | height: calc(100vh - 148px) !important;
17 | overflow-x: hidden !important;
18 | overflow-y: scroll !important;
19 | /* background: #e2e2e2; */
20 | }
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 |
5 | import { Provider } from 'react-redux'
6 | import Routes from './routes'
7 | import { store } from './store'
8 |
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | )
--------------------------------------------------------------------------------
/client/src/pages/cleanup.page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { bindActionCreators } from 'redux'
4 |
5 | import CleanUpNavBar from '../components/cleanup/cleanupSubNav'
6 | import CleanUpInfo from '../components/cleanup/cleanUpInfo'
7 | import { resetLogSideSheet } from '../store/actions/cleanUp.action'
8 | import LogSideSheet from '../components/LogSideSheet'
9 |
10 | import { connect } from 'react-redux'
11 |
12 | class CleanUpPage extends React.PureComponent {
13 | render () {
14 | const { resetLogSideSheet, isShowingSideSheet, logData } = this.props
15 | return <>
16 |
17 |
18 |
19 | >
20 | }
21 |
22 | }
23 |
24 | const mapStateToProps = state => {
25 | return {
26 | isShowingSideSheet: state.cleanup.isShowingSideSheet,
27 | logData: state.cleanup.responseData
28 | }
29 | }
30 |
31 | const mapDispatchToProps = dispatch => bindActionCreators(
32 | {
33 | resetLogSideSheet
34 | },
35 | dispatch
36 | )
37 |
38 | export default connect(mapStateToProps, mapDispatchToProps)( CleanUpPage )
--------------------------------------------------------------------------------
/client/src/pages/container.page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import SecondaryNavBar from '../components/SecondaryNavBar'
4 | import ContainerLists from '../components/container/lists'
5 | import GroupsList from '../components/groups/GroupsList'
6 |
7 | import {containerStatsProcess} from '../store/actions/stats.action'
8 |
9 | import {store} from '../store'
10 |
11 | import { bindActionCreators } from 'redux'
12 | import { connect } from 'react-redux'
13 | import { toggleDeleteModal, resetLogSideSheet } from '../store/actions/container.action'
14 | import LogSideSheet from '../components/LogSideSheet'
15 | import Modal from '../components/container/deleteModal'
16 |
17 | class ContainerPage extends React.PureComponent {
18 |
19 | componentDidMount () {
20 | store.dispatch(containerStatsProcess())
21 | }
22 |
23 | render () {
24 | const { showGroupsPage,showModal, selectedContainer, toggleDeleteModal,
25 | resetLogSideSheet, isShowingSideSheet, logData } = this.props
26 | return <>
27 |
28 |
29 | { showModal && }
30 |
31 | {
32 | showGroupsPage
33 | ?
34 | :
35 | }
36 |
37 | >
38 | }
39 |
40 | }
41 |
42 | const mapStateToProps = state => {
43 | return {
44 | showGroupsPage: state.groups.showGroupsPage,
45 | showModal: state.container.showModal,
46 | selectedContainer: state.container.selectedContainer,
47 | isShowingSideSheet: state.container.isShowingSideSheet,
48 | logData: state.container.logData
49 | }
50 | }
51 |
52 | const mapDispatchToProps = dispatch => bindActionCreators(
53 | {
54 | toggleDeleteModal,
55 | resetLogSideSheet
56 | },
57 | dispatch
58 | )
59 |
60 | export default connect(mapStateToProps, mapDispatchToProps)( ContainerPage )
--------------------------------------------------------------------------------
/client/src/pages/image.page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import SecondaryNavBar from '../components/SecondaryNavBar'
4 | import ImageLists from '../components/image/imageLists'
5 |
6 | class ImagePage extends React.PureComponent {
7 |
8 | render () {
9 | return <>
10 |
11 | >
12 | }
13 |
14 | }
15 |
16 | export default ImagePage
--------------------------------------------------------------------------------
/client/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {BrowserRouter, Route, Switch} from 'react-router-dom'
3 | import Navbar from './components/NavBar'
4 |
5 | import ContainerPage from './pages/container.page'
6 | import ImagePage from './pages/image.page'
7 | import CleanupPage from './pages/cleanup.page'
8 |
9 | const Routes = () => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default Routes
--------------------------------------------------------------------------------
/client/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/client/src/store/actions/cleanUp.action.js:
--------------------------------------------------------------------------------
1 | import { request } from '../../utilities/request'
2 | import { toaster } from 'evergreen-ui'
3 |
4 | export const setCleanUpSegment = payload => ({
5 | type: 'SELECTED_SEGMENT',
6 | payload
7 | })
8 |
9 | export const executePrune = payload => ({
10 | type: 'EXECUTE_PRUNE',
11 | payload
12 | })
13 |
14 |
15 | export const setSelectedCleanUpSegment = (value) => (dispatch, getState)=>{
16 | dispatch(setCleanUpSegment({
17 | segmentValue: value
18 | }))
19 | }
20 |
21 |
22 | export const pruneCommand = (type) => (dispatch, getState)=>{
23 | dispatch(executePrune({
24 | apiCallStarted: true,
25 | responseData: {},
26 | isShowingSideSheet:false,
27 | }))
28 | request('get', `cleanup/command?type=${type}`)
29 | .then(res => {
30 | dispatch(executePrune({
31 | isShowingSideSheet: res.data ? true : false,
32 | responseData: res.data,
33 | apiCallStarted: false
34 | }))
35 | if(!res.data){
36 | toaster.success(`${type} prune success`, {duration: 5})
37 | }
38 | })
39 | .catch(ex=>{
40 | dispatch(executePrune({
41 | responseData: {},
42 | isShowingSideSheet:false,
43 | apiCallStarted:false
44 | }))
45 | })
46 | }
47 |
48 | export const resetLogSideSheet = () => (dispatch, getState)=>{
49 | dispatch(executePrune({
50 | isShowingSideSheet: !getState().cleanup.isShowingSideSheet,
51 | apiCallStarted: getState().cleanup.apiCallStarted
52 | }))
53 | }
--------------------------------------------------------------------------------
/client/src/store/actions/container.action.js:
--------------------------------------------------------------------------------
1 | import { request } from '../../utilities/request'
2 | import { toaster } from 'evergreen-ui'
3 |
4 | export const genericContainer = payload => ({
5 | type: 'GENERIC_CONTAINER',
6 | payload
7 | })
8 |
9 | export const updateContainer = payload => ({
10 | type: 'UPDATE_CONTAINER',
11 | payload
12 | })
13 |
14 | export const removeContainer = payload => ({
15 | type: 'DELETE_CONTAINER',
16 | payload
17 | })
18 |
19 | export const updateContainerLog = payload => ({
20 | type: 'UPDATE_LOG',
21 | payload
22 | })
23 |
24 | export const toggleModal = payload => ({
25 | type: 'TOGGLE_MODAL',
26 | payload
27 | })
28 |
29 | export const getContainers = (status = 'active') => {
30 | return dispatch => {
31 | dispatch(genericContainer({
32 | loading: status,
33 | pageError: false,
34 | segment: status,
35 | activeIndex: 0,
36 | containerListLoading: true,
37 | }))
38 | request('get', `container/fetch?status=${status}`, {})
39 | .then(response => {
40 | dispatch(genericContainer({
41 | loading: false,
42 | containers: response.data,
43 | pageError: false,
44 | containerListLoading: false,
45 | }))
46 | }).catch(error => {
47 | dispatch(genericContainer({
48 | loading: false,
49 | pageError: true,
50 | containerListLoading: false,
51 | }))
52 | })
53 | }
54 | }
55 |
56 | export const toggleContainer = (container, status, hideToaster) => {
57 | return dispatch => {
58 | dispatch(updateContainer({
59 | containerId: container.shortId,
60 | data: { stateToggling: true },
61 | }))
62 | request('get', `container/command?container=${container.shortId}&command=${status}`)
63 | .then(res => {
64 | const State = {
65 | ...container.State,
66 | ...{
67 | Running: status === 'start'
68 | ? true
69 | : false
70 | }
71 | }
72 | dispatch(updateContainer({
73 | containerId: container.shortId,
74 | data: {
75 | State,
76 | stateToggling: false,
77 | },
78 | }))
79 | if(! !!hideToaster) {
80 | toaster.success(
81 | `Container ${container.Name} has been ${status === 'start'? 'started' : 'stopped'}.`,
82 | { duration: 5 }
83 | )
84 | }
85 | })
86 | .catch( ex => {
87 | dispatch(updateContainer({
88 | containerId: container.shortId,
89 | data: { stateToggling: false },
90 | }))
91 | toaster.warning(`There is problem ${status === 'start'? 'starting' : 'stopping'} container ${container.Name}`,{duration: 5})
92 | })
93 | }
94 | }
95 |
96 | export const restartContainer = (container, status) => {
97 | return dispatch => {
98 | dispatch(updateContainer({
99 | containerId: container.shortId,
100 | data: {
101 | stateToggling: true,
102 | State: {
103 | ...container.State,
104 | ...{
105 | Running: false
106 | }
107 | }
108 | },
109 | }))
110 | request('get', `container/command?container=${container.shortId}&command=${status}`)
111 | .then(res => {
112 | dispatch(updateContainer({
113 | containerId: container.shortId,
114 | data: {
115 | State: {
116 | ...container.State,
117 | ...{
118 | Running: true
119 | }
120 | },
121 | stateToggling: false,
122 | },
123 | }))
124 | toaster.success(`Container ${container.Name} has been restarted.`,{ duration: 5 })
125 | })
126 | .catch( ex => {
127 | dispatch(updateContainer({
128 | containerId: container.shortId,
129 | data: {
130 | State: {
131 | ...container.State,
132 | ...{
133 | Running: false
134 | }
135 | },
136 | stateToggling: false,
137 | },
138 | }))
139 | toaster.warning(`There is problem restarting container ${container.Name}`,{duration: 5})
140 | })
141 | }
142 | }
143 |
144 | export const deleteContainer = (container, command) => (dispatch, getState)=>{
145 | request('get', `container/command?container=${container.shortId}&command=${command}`)
146 | .then(res => {
147 | dispatch(removeContainer({
148 | containerId: container.shortId,
149 | showModal: !getState().container.showModal,
150 | selectedContainer: {}
151 | }))
152 | toaster.success(
153 | `Container ${container.Name} is no more!!!.`,
154 | {
155 | duration: 5
156 | }
157 | )
158 | })
159 | }
160 |
161 | export const getLog = (container) => {
162 | return dispatch => {
163 | dispatch(updateContainerLog({
164 | container: container,
165 | isShowingSideSheet: false,
166 | }))
167 | request('get', `container/logs?container=${container.shortId}`)
168 | .then(response => {
169 | dispatch(updateContainerLog({
170 | container: container,
171 | isShowingSideSheet: true,
172 | logData: response.data
173 | }))
174 | })
175 | }
176 | }
177 |
178 | export const resetLogSideSheet = () => (dispatch, getState)=>{
179 | dispatch(updateContainerLog({
180 | isShowingSideSheet: !getState().container.isShowingSideSheet,
181 | }))
182 | }
183 |
184 | export const toggleDeleteModal = (container) => (dispatch, getState)=>{
185 | dispatch(toggleModal({
186 | showModal: !getState().container.showModal,
187 | selectedContainer: container ? container : {}
188 | }))
189 | }
--------------------------------------------------------------------------------
/client/src/store/actions/groups.action.js:
--------------------------------------------------------------------------------
1 | import { store } from '../'
2 | import { request } from '../../utilities/request'
3 |
4 | export const genericGroups = payload => ({
5 | type: 'GENERIC_GROUPS',
6 | payload
7 | })
8 |
9 | export const groupItemSelector = itemID => {
10 | return dispatch => {
11 | const selectedItems = store.getState().groups.selectedItems
12 | if(selectedItems.includes(itemID)) {
13 | // Remove item.
14 | const modifiedListOfItems = selectedItems.filter(value => value != itemID)
15 | dispatch(genericGroups({
16 | selectedItems: modifiedListOfItems,
17 | }))
18 | } else {
19 | // Add item.
20 | const items = [
21 | ...selectedItems,
22 | itemID
23 | ]
24 | dispatch(genericGroups({
25 | selectedItems: items
26 | }))
27 | }
28 | }
29 | }
30 |
31 | export const groupStatusUpdater = (groupSchemaProperty, groupIndex, add) => {
32 | return dispatch => {
33 | const items = store.getState().groups[groupSchemaProperty]
34 | if(add) {
35 | // Remove the group index.
36 | const newItems = items.filter(value => value != groupIndex)
37 | dispatch(genericGroups({
38 | [groupSchemaProperty]: newItems
39 | }))
40 | } else {
41 | // Add the group index.
42 | const newItems = [
43 | ...items,
44 | groupIndex
45 | ]
46 | dispatch(genericGroups({
47 | [groupSchemaProperty]: newItems
48 | }))
49 | }
50 | }
51 | }
52 |
53 | export const createGroup = data => {
54 | return dispatch => {
55 | dispatch(genericGroups({ createFormLoading: true }))
56 | request('post', 'groups', {name: data.newGroupName, containers: data.selectedItems})
57 | .then(res => {
58 | setTimeout(() => {
59 | dispatch(genericGroups({
60 | newGroupName: '',
61 | selectedItems: [],
62 | showGroupsPage: true,
63 | showNewGroupForm: false,
64 | createFormLoading: false,
65 | }))
66 | }, 1200)
67 | })
68 | }
69 | }
70 |
71 | export const getGroups = () => {
72 | return dispatch => {
73 | dispatch(genericGroups({
74 | groupListLoading: true,
75 | }))
76 | request('get', 'groups', {})
77 | .then(res => {
78 | dispatch(genericGroups({
79 | groups: res.data,
80 | groupListLoading: false,
81 | }))
82 | })
83 | }
84 | }
85 |
86 | export const deleteGroup = groupId => {
87 | return dispatch => {
88 | request('delete', 'groups', {id: groupId})
89 | .then(res => {
90 | dispatch(getGroups())
91 | })
92 | }
93 | }
--------------------------------------------------------------------------------
/client/src/store/actions/image.action.js:
--------------------------------------------------------------------------------
1 | import { request } from '../../utilities/request'
2 | import { toaster } from 'evergreen-ui'
3 |
4 | export const genericImage = payload => ({
5 | type: 'GENERIC_IMAGE',
6 | payload
7 | })
8 |
9 | export const runImage = payload => ({
10 | type: 'RUN_IMAGE',
11 | payload
12 | })
13 |
14 | export const removeImage = payload => ({
15 | type: 'DELETE_IMAGE',
16 | payload
17 | })
18 |
19 | export const toggleModal = payload => ({
20 | type: 'TOGGLE_IMAGE_MODAL',
21 | payload
22 | })
23 |
24 | export const getImages = () => {
25 | return dispatch => {
26 | dispatch(genericImage({
27 | loading: true,
28 | pageError: false,
29 | activeIndex: 0,
30 | }))
31 | request('get', `image/fetch`)
32 | .then(response => {
33 | dispatch(genericImage({
34 | loading: false,
35 | images: response.data,
36 | pageError: false,
37 | }))
38 | }).catch(error => {
39 | dispatch(genericImage({
40 | loading: false,
41 | pageError: true,
42 | }))
43 | })
44 | }
45 | }
46 |
47 | export const runImageToContainer = (image) => {
48 | return dispatch => {
49 | dispatch(runImage({
50 | imageId: image.ID,
51 | data: { stateToggling: true },
52 | }))
53 | request('get', `image/command?image=${image.ID}&command=${'run'}`)
54 | .then(res => {
55 | dispatch(runImage({
56 | imageId: image.ID,
57 | data: { stateToggling: false },
58 | }))
59 | toaster.success(`Image ${image.ID} has been started running.`,{ duration: 5 })
60 | })
61 | .catch( ex => {
62 | dispatch(runImage({
63 | imageId: image.ID,
64 | data: { stateToggling: false },
65 | }))
66 | toaster.warning(`There is problem running image ${image.ID}`,{duration: 5})
67 | })
68 | }
69 | }
70 |
71 |
72 | export const deleteImage = (image, command) => (dispatch, getState)=>{
73 | request('get', `image/command?image=${image.ID}&command=${command}`)
74 | .then(res => {
75 | dispatch(removeImage({
76 | imageId: image.ID,
77 | showModal: !getState().image.showModal,
78 | selectedImage: {}
79 | }))
80 | toaster.success(
81 | `Image ${image.ID} is no more!!!.`,
82 | {
83 | duration: 5
84 | }
85 | )
86 | })
87 | .catch(ex=>{
88 | dispatch(removeImage({
89 | showModal: !getState().image.showModal,
90 | selectedImage: {}
91 | }))
92 | toaster.danger(
93 | `Image ${image.ID} can not be deleted! May be used by some containers.`,
94 | {
95 | duration: 5
96 | },
97 | )
98 | })
99 | }
100 |
101 | export const toggleImageDeleteModal = (image) => (dispatch, getState)=>{
102 | dispatch(toggleModal({
103 | showModal: !getState().image.showModal,
104 | selectedImage: image ? image : {}
105 | }))
106 | }
--------------------------------------------------------------------------------
/client/src/store/actions/stats.action.js:
--------------------------------------------------------------------------------
1 | import { store } from '../'
2 | import { request } from '../../utilities/request'
3 |
4 | export const genericStats = payload => ({
5 | type: 'GENERIC_STATS',
6 | payload
7 | })
8 |
9 | export const getContainersStat = () => {
10 | return dispatch => {
11 | request('get', `container/stats`, {})
12 | .then(response => {
13 | dispatch(genericStats({ containerStats: response.data }))
14 | }).catch(error => {
15 | console.log(error)
16 | })
17 | }
18 | }
19 |
20 | export const containerStatsProcess = () => {
21 | if(!store.getState().stats.isLive) {
22 | return dispatch => {
23 | dispatch(getContainersStat())
24 | dispatch(genericStats({ isLive: true }))
25 | setInterval(() => {
26 | dispatch(getContainersStat())
27 | }, 4000)
28 | }
29 | } else {
30 | return dispatch => {
31 | dispatch(genericStats({ isLive: true }))
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/client/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 |
4 | import schema from './schema'
5 | import rootReducer from './reducers'
6 | const middlewares = [thunk]
7 |
8 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
9 | const { createLogger } = require('redux-logger')
10 | const logger = createLogger({
11 | collapsed: true,
12 | })
13 | middlewares.push(logger)
14 | }
15 |
16 | export const store = createStore(
17 | rootReducer,
18 | schema, // Initial data.
19 | applyMiddleware(...middlewares)
20 | )
--------------------------------------------------------------------------------
/client/src/store/reducers/cleanUp.reducer.js:
--------------------------------------------------------------------------------
1 | export default (state = null, action) => {
2 |
3 | switch (action.type) {
4 |
5 | case 'SELECTED_SEGMENT':
6 | return {
7 | ...state,
8 | ...{
9 | selectedSegment: state.segmentOptions.find(c => {
10 | return c.value === action.payload.segmentValue
11 | })
12 | }
13 | }
14 |
15 | case 'EXECUTE_PRUNE':
16 | return {
17 | ...state,
18 | ...{
19 | isShowingSideSheet: action.payload.isShowingSideSheet,
20 | responseData: action.payload.responseData ? {
21 | data: action.payload.responseData
22 | } : {},
23 | apiCallStarted: action.payload.apiCallStarted
24 | }
25 | }
26 |
27 | default:
28 | return state
29 |
30 | }
31 | }
--------------------------------------------------------------------------------
/client/src/store/reducers/container.reducer.js:
--------------------------------------------------------------------------------
1 | export default (state = null, action) => {
2 |
3 | switch (action.type) {
4 |
5 | case 'GENERIC_CONTAINER':
6 | return {
7 | ...state,
8 | ...action.payload
9 | }
10 |
11 | case 'UPDATE_CONTAINER':
12 | return {
13 | ...state,
14 | ...{
15 | containers: state.containers.map(c => {
16 | if(c.shortId == action.payload.containerId) {
17 | return {
18 | ...c,
19 | ...action.payload.data
20 | }
21 | } else {
22 | return c
23 | }
24 | })
25 | }
26 | }
27 |
28 | case 'DELETE_CONTAINER':
29 | return {
30 | ...state,
31 | ...{
32 | containers: state.containers.filter(c => {
33 | return c.shortId !== action.payload.containerId
34 | },
35 | ),
36 | showModal: action.payload.showModal,
37 | selectedContainer: action.payload.selectedContainer
38 | }
39 | }
40 |
41 | case 'UPDATE_LOG':
42 | return {
43 | ...state,
44 | ...{
45 | logData: action.payload.logData && action.payload.container ? {
46 | container: action.payload.container ,
47 | data: action.payload.logData
48 | } : {},
49 | isShowingSideSheet: action.payload.isShowingSideSheet
50 | }
51 | }
52 |
53 | case 'TOGGLE_MODAL':
54 | return {
55 | ...state,
56 | ...{
57 | showModal: action.payload.showModal,
58 | selectedContainer: action.payload.selectedContainer
59 | }
60 | }
61 |
62 | default:
63 | return state
64 |
65 | }
66 | }
--------------------------------------------------------------------------------
/client/src/store/reducers/groups.reducer.js:
--------------------------------------------------------------------------------
1 | export default (state = null, action) => {
2 |
3 | switch (action.type) {
4 |
5 | case 'GENERIC_GROUPS':
6 | return {
7 | ...state,
8 | ...action.payload
9 | }
10 |
11 | default:
12 | return state
13 |
14 | }
15 | }
--------------------------------------------------------------------------------
/client/src/store/reducers/image.reducer.js:
--------------------------------------------------------------------------------
1 | export default (state = null, action) => {
2 |
3 | switch (action.type) {
4 |
5 | case 'GENERIC_IMAGE':
6 | return {
7 | ...state,
8 | ...action.payload
9 | }
10 |
11 | case 'RUN_IMAGE':
12 | return {
13 | ...state,
14 | ...{
15 | images: state.images.map(c => {
16 | if(c.ID == action.payload.imageId) {
17 | return {
18 | ...c,
19 | ...action.payload.data
20 | }
21 | } else {
22 | return c
23 | }
24 | })
25 | }
26 | }
27 |
28 | case 'DELETE_IMAGE':
29 | return {
30 | ...state,
31 | ...{
32 | images: state.images.filter(c => {
33 | if(action.payload.imageId) {
34 | return c.ID !== action.payload.imageId
35 | } else {
36 | return c
37 | }
38 | }),
39 | showModal: action.payload.showModal,
40 | selectedImage: action.payload.selectedImage
41 | }
42 | }
43 |
44 | case 'TOGGLE_IMAGE_MODAL':
45 | return {
46 | ...state,
47 | ...{
48 | showModal: action.payload.showModal,
49 | selectedImage: action.payload.selectedImage
50 | }
51 | }
52 |
53 | default:
54 | return state
55 |
56 | }
57 | }
--------------------------------------------------------------------------------
/client/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import stats from './stats.reducer'
4 | import groups from './groups.reducer'
5 | import container from './container.reducer'
6 | import image from './image.reducer'
7 | import cleanup from './cleanUp.reducer'
8 |
9 | const appReducer = combineReducers({
10 | stats,
11 | groups,
12 | container,
13 | image,
14 | cleanup
15 | })
16 |
17 | export default (state, action) => {
18 | return appReducer(state, action)
19 | }
--------------------------------------------------------------------------------
/client/src/store/reducers/stats.reducer.js:
--------------------------------------------------------------------------------
1 | export default (state = null, action) => {
2 |
3 | switch (action.type) {
4 |
5 | case 'GENERIC_STATS':
6 | return {
7 | ...state,
8 | ...action.payload
9 | }
10 |
11 | default:
12 | return state
13 |
14 | }
15 | }
--------------------------------------------------------------------------------
/client/src/store/schema/cleanup.schema.js:
--------------------------------------------------------------------------------
1 | export default {
2 | segmentOptions: [
3 | { label: 'Prune Images', value: 'image', message: 'This action will allow you to clean up unused images. It cleans up dangling images. A dangling image is one that is not tagged and is not referenced by any container.' },
4 | { label: 'Prune Containers', value: 'container', message: 'When you stop a container, it is not automatically removed unless you started it with the --rm flag. A stopped container’s writable layers still take up disk space.' },
5 | { label: 'Prune Volumes', value: 'volume', message: 'Volumes can be used by one or more containers, and take up space on the Docker host. Volumes are never removed automatically, because to do so could destroy data.' },
6 | { label: 'Prune System', value: 'system', message: 'Remove all unused containers, networks, images (both dangling and unreferenced), and optionally, volumes.' }
7 | ],
8 | selectedSegment: { label: 'Prune Images', value: 'image', message: 'This action will allow you to clean up unused images. It cleans up dangling images. A dangling image is one that is not tagged and is not referenced by any container.' },
9 | responseData: {},
10 | isShowingSideSheet: false,
11 | apiCallStarted: false
12 | }
--------------------------------------------------------------------------------
/client/src/store/schema/container.schema.js:
--------------------------------------------------------------------------------
1 | export default {
2 | containers: [],
3 | loading: false,
4 | containerListLoading: true,
5 | pageError: false,
6 | segment: 'active',
7 | activeIndex: 0,
8 | isShowingSideSheet: false,
9 | logData: {},
10 | showModal: false,
11 | selectedContainer: {}
12 | }
--------------------------------------------------------------------------------
/client/src/store/schema/groups.schema.js:
--------------------------------------------------------------------------------
1 | export default {
2 | groups: [],
3 | selectedItems: [],
4 | showGroupsPage: false,
5 | showNewGroupForm: false,
6 | activeIndex: 0,
7 | newGroupName: '',
8 | createFormLoading: false,
9 | groupListLoading: true,
10 | groupsRunning: [],
11 | groupsSwitchDisabled: [],
12 | }
--------------------------------------------------------------------------------
/client/src/store/schema/image.schema.js:
--------------------------------------------------------------------------------
1 | export default {
2 | images: [],
3 | loading: false,
4 | pageError: false,
5 | activeIndex: 0,
6 | isShowingSideSheet: false,
7 | logData: {},
8 | showModal: false,
9 | selectedImage: {}
10 | }
--------------------------------------------------------------------------------
/client/src/store/schema/index.js:
--------------------------------------------------------------------------------
1 | import stats from './stats.schema'
2 | import groups from './groups.schema'
3 | import container from './container.schema'
4 | import image from './image.schema'
5 | import cleanup from './cleanup.schema'
6 |
7 | export default {
8 | stats,
9 | groups,
10 | container,
11 | image,
12 | cleanup
13 | }
--------------------------------------------------------------------------------
/client/src/store/schema/stats.schema.js:
--------------------------------------------------------------------------------
1 | export default {
2 | containerStats: [],
3 | isLive: false,
4 | }
--------------------------------------------------------------------------------
/client/src/utilities/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | export const restPath = '/api/'
3 |
4 | export const request = ( method, path, data = {} ) => {
5 | const options = {
6 | method,
7 | data,
8 | url: restPath + path,
9 | timeout: 50000,
10 | }
11 | return axios(options)
12 | }
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | docker-web-gui:
5 | container_name: docker-web-gui
6 | build:
7 | context: .
8 | ports:
9 | - "3230:3230"
10 | volumes:
11 | - /var/run/docker.sock:/var/run/docker.sock
12 | restart: always # This ensures the container auto-starts and restarts on failure
13 |
--------------------------------------------------------------------------------